多线程的基本概念和Delphi线程对象Tthread介绍
多线程的两个概念: 1) 进程:也称任务,程序载入内存,并分配资源,称为“一个进程”。 注意:进程本身并不一定要正在执行。进程由以下几部分组成: a>一个私有的地址空间,它是进程可以使用的一组虚拟内存地址空间; b>程序的相关代码、数据源; c>系统资源,比如操作系统同步对象等; d>至少包含一个线程(主线程);
2) 线程:是进程的执行单位(线程本身并不包括程序代码,真正拥有代码的是进程),是操作系统分配CPU时间的基本实体,每个进程至少包括一个线程,称为主线程。一个进程如果有多个线程,就可以共享同一进程的资源,并可以并发执行。通俗点说就是进程中一段并发运行的代码(一个函数或过程)。
线程主要由如下两部分组成: a>数据结构; b>CPU 寄存器和堆栈;
线程函数运行,启动函数就返回了,主线程继续向下执行,而线程函数在一个独立的线程中执行,它要执行多久,什么时候返回,主线程是不管也不知道的。
一、Delphi线程对象--- Tthread
虽然Windows提供了较多的多线程设计的API 函数,但是直接使用API 函数极其不方便,而且使用不当还容易出错。为解决这个问题,Borland公司率先推出了一种Tthread 对象,来解决多线程设计上的困难,简化了多线程问题的处理。
一、Tthread对象的主要方法
构造线程:
constructor Create(CreateSuspended:boolean)
CreateSuspended=true构造但不唤醒 ;false构造的同时即唤醒 。
挂起线程: suspend:(把线程挂起的次数加一)
唤醒线程: resume :(注意:注意这个属性是把线程挂起的次数减一,当次数为0时,即唤醒。也就是说,线程挂起多少次,唤醒也需要多少次。同时挂起的时候将保持线程的地址指针不变,所以线程挂起后再唤醒,将从挂起的地方开始运行)
析构(清除线程所占用的内存): destroy
终止线程 Terminate
使用这个类也很简单,基本用法是:先从TThread派生一个自己的线程类(因为TThread 是一个抽象类,不能生成实例),然后是覆盖(Override)抽象方法:Execute(这就是线程函数,也就是在线程中执行的代码部分),如果需要用到可视VCL对象,还需要通过Synchronize过程进行。
线程的终止和退出:
1)自动退出:
一个线程从Execute()过程中退出,即意味着线程的终止,此时将调用Windows的ExitThread()函数来清除线程所占用的堆栈。
如果线程对象的 FreeOnTerminate属性设为True,则线程对象将自动删除,并释放线程所占用的资源。 这是消除线程对象最简单的办法。
2)受控退出:
利用线程对象的Terminate属性,可以由进程或者由其他线程控制线程的退出。只需要简单的调用该线程的Terminate方法,并设置线程对象的Terminate属性为True。 一般来说,在线程中,应该不断监视Terminate的值,一旦发现为True,则退出,一般来说,例如在Execute()过程中可以这样写:
While not Terminate do begin ........ end;
3)退出的API函数:
关于线程退出的API函数声明如下:
Function TerminateThread(hThread:Thandle;dwExitCode:DWORD);
不过,这个函数会使代码立刻终止,而不管程序中有没有
try....finally
机制,可能会导致错误,不到万不得已,最好不要使用。
4)利用挂起线程的方法(suspend)
利用挂起线程的suspend方法,后面跟个Free,也可以释放线程, 例如:
thread1.suspend; //挂起
thread2.free; //释放
二、多线程的同步机制
同步机制,研究多线程的同步机制的必要性在于,多线程同步工作时,如果同时调用相同的资源,就可能会出现问题,如对全局变量、数据库操作发生冲突,甚至产生死锁和竞争问题。
举个发生冲突的实例看一下:
一般来说,对内存数据加一的操作分解以后有三个步骤: 1、从内存中读出数据 2、数据加一 3、存入内存 现在假设在一个两个线程的应用中用Inc进行加一操作可能出现的一种情况: 1、线程A从内存中读出数据(假设为3) 2、线程B从内存中读出数据(也是3) 3、线程A对数据加一(现在是4) 4、线程B对数据加一(现在也是4) 5、线程A将数据存入内存(现在内存中的数据是4) 6、线程B也将数据存入内存(现在内存中的数据还是4,但两个线程都对它加了一,应该是5才对,所以这里出现了错误的结果)
1.临界区(Critical Sections)
临界区(CriticalSection)是一项共享数据访问保护的技术。对它只有两个操作:Enter和Leave,这两个操作也是原子操作。
它的保护原理是这样的:当一个线程A调用某一个Enter后,开始访问某个数据D,如果此时另一个线程B也要访问数据D,则它会在调用这个Enter时,发现已经有线程进入临界区,然后线程B就会被挂起,等待线程A调用Leave。当线程A完成操作,调用Leave离开后,线程B就会被唤醒,并设置临界区标志,开始操作数据,这样就防止了访问冲突
var CS:TRTLCriticalSection;//被声明在程序最上方,作为线程都可以使用的全局变量。 initializeCriticalSection(cs); //初始化
Procedure InterlockedIncrement( var aValue : Integer ); Begin EnterCriticalSection(CS);//独占 Inc( aValue ); LeaveCriticalSection(CS); //解除独占 End;
现在再来看前面那个例子: 1. 线程A进入临界区(假设数据为3) 2. 线程B进入临界区,因为A已经在临界区中,所以B被挂起 3. 线程A对数据加一(现在是4) 4. 线程A离开临界区,唤醒线程B(现在内存中的数据是4) 5. 线程B被唤醒,对数据加一(现在就是5了) 6. 线程B离开临界区,现在的数据就是正确的了。 临界区就是这样保护共享数据的访问
请注意,临界区只能在一个进程内使用,可以在多处设置调用enter。
不要长时间锁住一份资源,如果你一直让资源被锁定,你就会阻止其它线程的执行,并把整个程序带到一个完全停止的状态,所以千万不要在一个ciritical section中调用sleep()或任何Wait…()函数。
ciritical section的一个缺点是,它不是核心对象,如果进入ciritical section的那个线程结束了或者当掉了,而没有调用LeaveCriticalSection的话,系统没有办法将该ciritical section清除,如果你需要这样的机能,你应该使用mutex。
VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//一个指针,指向欲被初始化的CRITICAL_SECTION变量
);
函数功能:初始化一个临界对象,当你用毕临界对象时,必须调用DeleteCriticalSection()清除它。
VOID DeleteCriticalSection ( LPCRITICAL_SECTION lpCriticalSection// 临界对象指针 );
函数功能:申请删除临界对象
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//临界对象指针
);
函数功能:申请进入临界对象
VOID LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection//临界对象指针
);
函数功能
申请进入临界对象
2.互斥器(Mutexes)
一个时间内只能够有一个线程拥有mutex,就好像同一个时间只能够有一个线程进入同一临界区。
Mutex和critical section还是有差别的:
1.锁住一个未被使用的Mutexes,比锁住一个未被使用的critical section,需要花费几乎100倍的时间
2. Mutexes可以跨进程使用,critical section则只能够在同一个进程中使用
3.等待一个Mutexes时,你可以指定结束等待的时间长度,但对于critical section则不行。
两种对象的相关函数比较:
Mutex的使用机制:
1. 有一个mutex,此时没有任何线程拥有它,此时它处于非激发状态。
2. 某个线程调用WaitforSingleObject()或任何其它的wait…函数,并指定该mutex的handle为参数
3. win32于是将该mutex的拥有权给予这个线程,然后将此mutex设为激发状态,于是wait..函数返回
4. wait..函数返回后,win32立刻又将mutex设为非激发状态,是任何处于等待状态下的其它线程没有办法获得其拥有权
5. 获得该mutex的线程调用Release,将mutex释放掉。于是循环到第一步。
如果线程拥有一个mutex,而在结束前没有调用releaseMutex,mutex不会被摧毁,该mutex会被win32视为“未被拥有”以及“未被激发”,下一个等待中的线程会被以WAIT_ABANDONED_0通知。如果是WaitForMultipleObjects()等待辞mutex,函数返回值介于WAIT_ABANDONED_0和WAIT_ABANDONED_0+n之间,n是指handle数组的元素个数。
HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName );
参数
lpMutexAttributes:安全属性。Null表示使用默认的属性。
bInitialOwner:如果你希望调用这个函数的线程拥有mutex,就将此值设为true
lpName:互斥对象的名称
返回值
如果成功,则返回一个handle,否则返回null。
函数说明:
如果指定的mutex名称已经存在,win32会给你一个mutex handle,而不会为你产生一个新的mutex。调用GetLastError会传回ERROR_ALREADY_EXISTS。当你不需要mutex时,你可以调用closehandle()将它关闭。
BOOL ReleaseMutex(
HANDLE hMutex //欲释放mutex的handle
);
返回值
如果成功,则返回true,否则返回false。
3.信号量(Semaphores)
Mutex是semaphore的一种退化,如果你产生一个semaphore并令最大值为1,那它就是个mutex。因此,mutex又常被称为binary semaphore。在许多系统中,semaphore常被使用,因为mutex可能并不存在,在win32中semaphore被使用的情况就少得多,因为mutex存在的原因。
一旦semaphore的现值降到0,就表示资源已经耗尽。此时任何线程如果调用Wait…函数,必然要等待,直到某个锁定被解除。
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
)
参数:
lpSemaphoreAttributes:安全属性,null表示使用默认属性。
lInitialCount:初始值,必须>=0,并且<= lMaximumCount
lMaximumCount:最大值,也就是在同一时间内能够锁住semaphore的线程数
lpName:名称,这个值也可以是null。
返回值:
如果成功就返回一个handle,否则返回null。如果semaphore名称已经存在,函数还是会成功,GetLastError会返回ERROR_ALREADY_EXISTS
函数说明:
产生一个semaphore。
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
参数:
hSemaphore:semaphore的句柄。
lReleaseCount:semaphore现值的增量,通常是1,该值不可以是负值或者0
lpPreviousCount:返回semaphore增加前的现值
返回值:
如果成功就返回true,否则返回false。
函数说明:
三、事件(Events)
事件(Event)是一种核心对象,它的唯一目的就是成为激发状态或未激发状态。这两种状态完全在你的掌握之下,不会因为Wait…函数的调用而变化。
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,
LPCTSTR lpName
);
参数:
lpEventAttributes:安全属性,null表示使用默认属性。
bManualReset :
此值为false,表示event变成激发状态后,自动重置为非激发状态;
此值为true,表示不会自动重置,必须靠程序(调用ResetEvent)操作才能将激发状态的event重置为非激发状态。
bInitialState :初始状态,true一开始处于激发状态,false一开始处于非激发状态
lpName :Event对象名
返回值:
如果成功就返回一个handle,否则返回null。如果event名称已经存在,函数还是会成功,GetLastError会返回ERROR_ALREADY_EXISTS
BOOL SetEvent(HANDLE hEvent);
//把event对象设为激发状态
BOOL ResetEvent(HANDLE hEvent);
//把event对象设为非激发状态
BOOL PulseEvent(HANDLE hEvent );
//如果event的bManualReset 为true:把event对象设为激发状态,唤醒所有等待中的线程,然后event恢复为非激发状态
//如果event的bManualReset 为false:把event对象设为激发状态,唤醒一个等待中的线程,然后event恢复为非激发状态
5.使用Synchronize方法
这个方法用于访问VCL主线程所管理的资源,其方法的应用是: 第一步:把访问主窗口(或主窗口控件资源)的代码放到线程的一个方法中; 第二步:是在线程对象的Execute方法中,通过Synchronize方法使用该方法。 实例: procedure Theater.Execute; begin Synchronize(update); end;
procedure Theater.update; begin ......... end;
这里通过 Synchronize使线程方法update同步。
6、使用VCL类的Look方法
在Delphi的IDE提供的构件中,有一些对象内部提供了线程的同步机制,工作线程可以直接使用这些控件,比如:Tfont,Tpen,TBitmap,TMetafile,Ticon等。另外,一个很重要的控件对象叫TCanvas,提供了一个Lock方法用于线程的同步,当一个线程使用此控件对象的时候,首先调用这个对象的Lock方法,然后对这个控件进行操作,完毕后再调用Unlock方法,释放对控间的控制权。 例如: CanversObject.look; try 画图 finally CanversObject.unlock; end; {使用这个保护机制,保证不论有没有异常,unlock都会被执行否则很可能会发生死锁。在多线程设计的时候,应该很注意发生死锁的问题}
四、线程的优先级:
在多线程的情况下,一般要根据线程执行任务的重要性,给线程适当的优先级,一般如果量的线程同时申请CPU时间,优先级高的线程优先。
优先权类别(Priority Class)
Win32提供四种优先权类别,每一个类别对应一个基本的优先权层次。
表格5-1优先权类别
大部分程序使用NORMAL_PRIORITY_CLASS。优先权类别是进程的属性之一,利用SetPriorityClass和GetPriorityClass函数可以调整和获取该值。
优先权层次(priority Level)
线程的优先权层次使你能够调整同一个进程内的各线程的相对重要性。一共七种优先权层次:
表格5-2 利用SetThreadPriority和GetThreadPriority函数可以调整和获取该值
在Windows下,给线程的优先级分为30级,而Delphi中Tthread对象相对简单的把优先级分为七级。也就是在Tthread中声明了一个枚举类型TTthreadPriority:
type TTthreadPriority(tpidle,tpLowest,tpLower,tpNormal, tpHight,tpHighest,tpTimecrital)
分别对应的是最低(系统空闲时有效,-15),较低(-2),低(-1),正常(普通0),高(1),较高(2),最高(15)。 设置优先级可使用thread对象的priority属性: threadObject.priority:=Tthreadpriority(级别);
BOOL SetThreadPriority(
HANDLE hThread, //欲调整优先权的那个线程的句柄
int nPriority //表格5-2所显示的值
);
Int GetThreadPriority(
HANDLE hThread //线程的句柄
);
返回值是线程的优先级。