Win32 API下的多线程编程
1. 用Win32函数创建和终止线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId);
如果创建成功则返回线程的句柄,否则返回NULL。创建了新的线程后,该线程就开始启动执行了。
但如果在dwCreationFlags中使用了CREATE_SUSPENDED特性,那么线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程,在这个过程中可以调用下面这个函数来设置线程的优先权:
BOOL SetThreadPriority(HANDLE hThread,int nPriority);
当调用线程的函数返回后,线程自动终止。如果需要在线程的执行过程中终止则可调用函数:
VOID ExitThread(DWORD dwExitCode);
如果在线程的外面终止线程,则可调用下面的函数:
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
但应注意: 该函数可能会引起系统不稳定,而且线程所占用的资源也不释放。因此,一般情况下,建议不要使用该函数。
如果要终止的线程是进程内的最后一个线程,则线程被终止后相应的进程也应终止。
2. 线程的同步
最常用的等待函数是:
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
而函数WaitForMultipleObject可以用来同时监测多个同步对象,该函数的声明为:
DWORD WaitForMultipleObject(DWORD nCount,CONST HANDLE *lpHandles,BOOL bWaitAll,DWORD dwMilliseconds);
(1)互斥体对象
Mutex对象的状态在它不被任何线程拥有时才有信号,而当它被拥有时则无信号。Mutex对象很适合用来协调多个线程对共享资源的互斥访问。可按下列步骤使用该对象:
首先,建立互斥体对象,得到句柄:
HANDLE CreateMutex();
然后,在线程可能产生冲突的区域前(即访问共享资源之前)调用WaitForSingleObject,将句柄传给函数,请求占用互斥对象:
dwWaitResult = WaitForSingleObject(hMutex,5000L);
共享资源访问结束,释放对互斥体对象的占用:
ReleaseMutex(hMutex);
互斥体对象在同一时刻只能被一个线程占用,当互斥体对象被一个线程占用时,若有另一线程想占用它,则必须等到前一线程释放后才能成功。
(2)信号对象
信号对象允许同时对多个线程共享资源进行访问,在创建对象时指定最大可同时访问的线程数。当一个线程申请访问成功后,信号对象中的计数器减一,调用ReleaseSemaphore函数后,信号对象中的计数器加一。
如果一个应用在创建一个信号对象时,将其计数器的初始值设为0,就阻塞了其他线程,保护了资源。等初始化完成后,调用ReleaseSemaphore函数将其计数器增加至最大值,则可进行正常的存取访问。可按下列步骤使用该对象:
首先,创建信号对象:
HANDLE CreateSemaphore();
或者打开一个信号对象:
HANDLE OpenSemaphore();
然后,在线程访问共享资源之前调用WaitForSingleObject。
共享资源访问完成后,应释放对信号对象的占用:
ReleaseSemaphore();
(3)事件对象
事件对象(Event)是最简单的同步对象,它包括有信号和无信号两种状态。在线程访问某一资源之前,需要等待某一事件的发生,这时用事件对象最合适。例如:只有在通信端口缓冲区收到数据后,监视线程才被激活。
事件对象是用CreateEvent函数建立的。该函数可以指定事件对象的类和事件的初始状态。如果是手工重置事件,那么它总是保持有信号状态,直到用ResetEvent函数重置成无信号的事件。如果是自动重置事件,那么它的状态在单个等待线程释放后会自动变为无信号的。用SetEvent可以把事件对象设置成有信号状态。在建立事件时,可以为对象命名,这样其他进程中的线程可以用OpenEvent函数打开指定名字的事件对象句柄。
(4)排斥区对象
在排斥区中异步执行时,它只能在同一进程的线程之间共享资源处理。虽然此时上面介绍的几种方法均可使用,但是,使用排斥区的方法则使同步管理的效率更高。
使用时先定义一个CRITICAL_SECTION结构的排斥区对象,在进程使用之前调用如下函数对对象进行初始化:
VOID InitializeCriticalSection(LPCRITICAL_SECTION);
当一个线程使用排斥区时,调用函数:EnterCriticalSection或者TryEnterCriticalSection;
当要求占用、退出排斥区时,调用函数LeaveCriticalSection,释放对排斥区对象的占用,供其他线程使用。
(5)自旋锁
自旋锁有四个明显的事实:
第一,如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁(deadlock)。自旋锁没有与其关联的“使用计数器”或“所有者标识”;
第二,锁或者被占用或者空闲。如果你在锁被占用时获取它,你将等待到该锁被释放。如果碰巧你的CPU已经拥有了该锁,那么用于释放锁的代码将得不到运行,因为你使CPU永远处于“测试并设置”某个内存变量的自旋状态。
第三,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。所以,为了避免影响性能,你应该在拥有自旋锁时做尽量少的操作,因为此时某个CPU可能正在等待这个自旋锁。
第四,你仅能在低于或等于DISPATCH_LEVEL级上请求自旋锁,在你拥有自旋锁期间,内核将把你的代码提升到DISPATCH_LEVEL级上运行。在内部,内核能在高于DISPATCH_LEVEL的级上获取自旋锁,但你和我都做不到这一点。
使用自旋锁
为了明确地使用一个自旋锁,首先要在非分页内存中为一个KSPIN_LOCK对象分配存储。然后调用KeInitializeSpinLock初始化这个对象。接着,当代码运行在低于或等于DISPATCH_LEVEL级上时获取这个锁,并执行需要保护的代码,最后释放自旋锁。例如,假设你的设备扩展中有一个名为QLock的自旋锁,你用它来保护你专用IRP队列的访问。你应该在AddDevice函数中初始化这个锁:
typedef struct _DEVICE_EXTENSION { ... KSPIN_LOCK QLock;} DEVICE_EXTENSION, *PDEVICE_EXTENSION;...NTSTATUS AddDevice(...){ ... PDEVICE_EXTENSION pdx = ...; KeInitializeSpinLock(&pdx->QLock); ...}
在驱动程序的其它地方,假定就在某种IRP的派遣函数中,你在某些必须的队列操作代码周围获取了(并很快释放)该自旋锁。注意这个函数必须存在于非分页内存中,因为在某个时期它会执行在提升的IRQL上。
NTSTATUS DispatchSomething(...)
{
KIRQL oldirql;
PDEVICE_EXTENSION pdx = ...;
KeAcquireSpinLock(&pdx->QLock, &oldirql);
... KeReleaseSpinLock(&pdx->QLock, oldirql);
}
当KeAcquireSpinLock获取自旋锁时,它也把IRQL提升到DISPATCH_LEVEL级上。
当KeReleaseSpinLock释放自旋锁时,它也把IRQL降低到原来的IRQL级上。
如果你知道代码已经处在DISPATCH_LEVEL级上,你可以调用两个专用函数来获取自旋锁。这个技术适合于DPC、StartIo,和其它执行在DISPATCH_LEVEL级上的驱动程序例程:
(6)原子操作
InterlockedIncrement();
基于MFC的多线程编程
在MFC中,线程分为两种:工作线程和用户接口线程。工作线程与前面所述的线程一致,用户接口线程是一种能够接收用户的输入、处理事件和消息的线程。
工作线程编程较为简单,设计思路与前面所讲的基本一致: 一个基本函数代表了一个线程,创建并启动线程后,线程进入运行状态; 如果线程用到共享资源,则需要进行资源同步处理。这种方式创建线程并启动线程时可调用函数:
CWinThread*AfxBeginThread( AFX_THREADPROC pfnThreadProc,
LPVOID pParam,int nPriority= THREAD_PRIORITY_NORMAL,
UINT nStackSize =0,DWORD dwCreateFlags=0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
参数pfnThreadProc是线程执行体函数,函数原形为: UINT ThreadFunction( LPVOID pParam)。
参数pParam是传递给执行函数的参数;
参数nPriority是线程执行权限,可选值:
THREAD_PRIORITY_NORMAL、THREAD_PRIORITY_LOWEST、THREAD_PRIORITY_HIGHEST、THREAD_PRIORITY_IDLE。
参数dwCreateFlags是线程创建时的标志,可取值CREATE_SUSPENDED,表示线程创建后处于挂起状态,调用ResumeThread函数后线程继续运行,或者取值“0”表示线程创建后处于运行状态。
返回值是CWinThread类对象指针,它的成员变量m_hThread为线程句柄,在Win32 API方式下对线程操作的函数参数都要求提供线程的句柄,所以当线程创建后可以使用所有Win32 API函数对pWinThread->m_Thread线程进行相关操作。
注意:如果在一个类对象中创建和启动线程时,应将线程函数定义成类外的全局函数。
2. 用户接口线程
基于MFC的应用程序有一个应用对象,它是CWinApp派生类的对象,该对象代表了应用进程的主线程。当线程执行完并退出线程时,由于进程中没有其他线程存在,进程自动结束。类CWinApp从CWinThread派生出来,CWinThread是用户接口线程的基本类。我们在编写用户接口线程时,需要从CWinThread派生我们自己的线程类,ClassWizard可以帮助我们完成这个工作。
先用ClassWizard派生一个新的类,设置基类为CwinThread。注意:类的DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏是必需的,因为创建线程时需要动态创建类的对象。根据需要可将初始化和结束代码分别放在类的InitInstance和ExitInstance函数中。
如果需要创建窗口,则可在InitInstance函数中完成。然后创建线程并启动线程。
可以用两种方法来创建用户接口线程,MFC提供了两个版本的AfxBeginThread函数,其中一个用于创建用户接口线程。
第二种方法分为两步进行:首先,调用线程类的构造函数创建一个线程对象;其次,调用CWinThread::CreateThread函数来创建该线程。线程建立并启动后,在线程函数执行过程中一直有效。如果是线程对象,则在对象删除之前,先结束线程。CWinThread已经为我们完成了线程结束的工作。
3. 线程同步
前面我们介绍了Win32 API提供的几种有关线程同步的对象,在MFC类库中对这几个对象进行了类封装,它们有一个共同的基类CSyncObject。
它们的对应关系为: Semaphore对应CSemaphore、Mutex对应CMutex、Event对应CEvent、CriticalSection对应CCriticalSection。另外,MFC对两个等待函数也进行了封装,即CSingleLock和CMultiLock。因四个对象用法相似,在这里就以CMutex为例进行说明:
创建一个CMutex对象:
CMutex mutex(FALSE,NULL,NULL);
或CMutex mutex;
当各线程要访问共享资源时使用下面代码:
CSingleLock sl(&mutex);
sl.Lock();
if(sl.IsLocked())
//对共享资源进行操作...
sl.Unlock();
结束语
如果用户的应用程序需要多个任务同时进行相应的处理,则使用多线程是较理想的选择。这里,提醒大家注意的是在多线程编程时要特别小心处理资源共享问题以及多线程调试问题.