window多线程及同步实现

来源:互联网 发布:数据库管理员要学什么 编辑:程序博客网 时间:2024/06/06 02:36

1. 关于线程的讨论
1.1 Windows98下的多任务、多进程和多线程
Windows98是一个多任务操作系统,它支持两种类型的多任务:基于进程(process)
的多任务和基于线程(thread)的多任务。进程是指正在执行的程序,在Windows98中
可以同时执行两个或多个任务进程,即运行多个程序,这就是普遍理解的基于进程
的多任务。
线程是指进程内的一条执行线路,或者说是进程中可执行代码的单独单元,它
是操作系统的基本调度单元。一个进程至少有一个线程,即主线程,也可以有多个
线程协同工作。进程从主线程开始执行,进而可以创建一个或多个附加线程来执行
该进程内的并发任务,这就是基于线程的多任务。
在Windows98中,每个Win32进程在独立的进程空间上运行,可以使用多达4G
的线性地址空间。但进程本身是一个静态的概念,为了完成相应操作必须占有相应
的线程,正是线程负责执行包含在进程地址空间中的代码。一个进程内的所有线程
使用相同的32位线性地址空间,并共享所有的进程资源(包括打开的文件和动态分
配的内存),但每个线程有自己的堆栈和CPU寄存器,它们的执行由系统调度程序
控制。
基于线程的多任务允许一个程序的两个或多个部分同时执行,这样增加了程序
的维数,用户可以通过定义分离的执行线路来管理程序的执行,使编写出来的程序
更高效。例如,通常为后台计算创建一个线程。特别的,在多处理器系统中,多线
程可以极大地提高系统效率。

1.2 进程与线程的优先级
每个进程和线程都有相应的优先级设置,线程的优先级决定它何时运行和接收
多少CPU时间。最终的优先级共32级,是从0到31的数值,称为基本优先级别(base
priority level)。系统按照不同的优先级调度线程的运行。
0-15级是普通优先级,线程的优先级可以动态变化,高优先级线程优先运行,
只有高优先级线程不运行时,才调度低优先级线程运行。优先级相同的线程按照时
间片轮流运行。
16-31级是实时优先级,实时优先级与普通优先级的最大区别在于相同优先级线
程的运行不按照时间片轮转,而是先运行的线程就先控制CPU,如果它不主动放弃
控制,同级或低优先级的线程就无法运行。
线程的实际优先级设置是两个值的结合:所属的进程的总优先级类和线程本身
的优先级别。一个线程的优先级首先属于一个类,然后是其在该类中的相对位置。
即:
线程优先级 = 进程优先级类 + 线程相对优先级
进程的优先级类从高到低如下(数字为基本优先级别数量):
REALTIME_PRIORITY_CLASS 24
HIGH_PRIORITY_CLASS 13
NORMAL_PRIORITY_CLASS 前台为9,后台为7
IDLE_PRIORITY_CLASS 4
进程的相对优先级:
THREAD_PRIORITY_LOWEST 比所在进程优先级低两个级别
THREAD_PRIORITY_BELOW_NORMAL 比所在进程优先级低一个级别
THREAD_PRIORITY_NORMAL 与进程同级
THREAD_PRIORITY_ABOVE_NORMAL 比所在进程优先级高一个级别
THREAD_PRIORITY_HIGHEST 比所在进程优先级高两个级别
HREAD_PRIORITY_IDLE 基本优先级为1。对于
REALTIME_PRIORITY_CLASS进程,优先级为16
THREAD_PRIORITY_TIME_CRITICAL 基本优先级为15。对于

REALTIME_PRIORITY_CLASS进程,优先级别是31。
在一般情况下,进程和线程的优先级被设置成NORMAL_PRIORITY_CLASS和
THREAD_PRIORITY_NORMAL,后面可以看到,程序运行时可以获取和更改进程
或线程的优先级。

1.3 线程同步
程序的并发执行往往带来与时间有关的错误,甚至引发灾难性的后果。这需要
引入同步机制。使用多进程与多线程时,有时需要协同两种或多种动作,此过程就
称同步(Synchronization)。引入同步机制的第一个原因是为了控制线程之间的资源
同步访问,因为多个线程在共享资源时如果发生访问冲突通常会带来不正确的后果。
例如,一个线程正在更新一个结构,同时另一个线程正试图读取同一个结构。结果,
我们将无法得知所读取的数据是新的还是旧的,或者是二者的混合。第二个原因是
有时要求确保线程之间的动作以指定的次序发生,如一个线程需要等待由另外一个
线程所引起的事件。
为了在多线程程序中解决同步问题,Windows98提供了四种主要的同步对象,
每种对象相对于线程有两种状态——
信号状态(signal state)和非信号状态(nonsignal
state)。
当相关联的同步对象处于信号状态时,线程可以执行(访问共享资源),反
之必须等待。这四种同步对象是:
(1)事件对象(Event)。事件对象作为标志在线程间传递信号。一个或多个线
程可等待一个事件对象,当指定的事件发生时,事件对象通知等待线程可以开始执
行。它有两种类型:自动重置(auto-reset)事件和手动重置(manual-reset)事件。
(2)临界区(Critical Section)。临界区对象通过提供一个进程内所有线程必须
共享的对象来控制线程。只有拥有那个对象的线程可以访问保护资源。在另一个线
程可以访问该资源之前,前一个线程必须释放临界区对象,以便新的线程可以索取
对象的访问权。
(3)互斥量(Mutex Semaphore)。互斥量的工作方式非常类似于临界区,只是
互斥量不仅保护一个进程内为多个线程使用的共享资源,而且还可以保护系统中两
个或多个进程之间的的共享资源。
(4)信号量(Semaphore)。信号量可以允许一个或有限个线程访问共享资源。
它是通过计数器来实现的,初始化时赋予计数器以可用资源数,当将信号量提供给
一个线程时,计数器的值减1,当一个线程释放它时,计数器值加1。当计数器值小
于等于0时,相应线程必须等待。信号量是Windows98同步系统的核心。从本质上
讲,互斥量是信号量的一种特殊形式。
Windows98/NT还提供了另外一种Windows95没有的同步对象:可等待定时器
(Waitable Timer)。它可以封锁线程的执行,直到到达某一具体时间。这可以用于
后台任务。
同步问题是多线程编程中最复杂的问题,本文后面将有详细的实现方法讨论。

1.4 用Visual C++6.0开发Windows应用程序的两种方法
用VC++6开发Windows98应用程序有两种方法,一种是利用Win32 API函数
编写C风格的Win32应用程序,另一种是利用MFC(Microsoft Foundation Classes)
类库编写C++风格的Win32应用程序。两种方法各有优缺点,基于Win32 API的应
用程序代码小巧,运行效率高,但开发难度较大;基于MFC的应用程序开发速度较
快,但代码很庞大。现在,用MFC类库进行开发越来越流行了。
这两种方法都可用来开发Windows98多线程应用程序,并且在原理上是一致的。
本文将分别讨论基于Win32 API的多线程编程和基于MFC的多线程编程。

2. 基于Win32 API的多线程编程
一个进程至少有一个线程,这个线程称为主线程。主线程可以创建一个或多个
辅助线程。Win32 API函数库中提供了多线程控制函数,下面将予以讨论。

2.1 编写线程函数
每个线程的执行都起始于在进程内对线程函数的调用,线程函数就是新执行线
程的入口点(线程起始地址)。线程函数应具有下面的原型:
DWORD WINAPI MyThreadFunc(LPVOID param);
用户需要在自己的程序中编写这个函数。

2.2 创建和终止线程
任何线程都可以通过调用WIN32 API函数CreateThread()来创建线程,其原型
如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性指针
DWORD dwStackSize,
// 堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress, // 线程起始地址
LPVOID lpParameter,
// 传递给线程的参数
DWORD dwCreationFlags,
// 起始执行状态
LPDWORD lpThreadId);
// 线程ID指针
安全属性(lpThreadAttributes指定)为Windows NT特有,Windows98忽略,
选NULL即可;dwStackSize如为0,则堆栈大小与主线程相同;dwCreationFlags有
两种情况,0表示立即执行,CREATE_SUSPENDED表示创建后被挂起,直到调用
ResumeThread()。该函数如果操作成功则返回线程的句柄,失败则返回NULL。
线程被创建后,正常情况下就运行直到线程函数返回。用户也可调用下面两个
函数来终止线程:
VOID ExitThread(DWORD dwStatus); //dwStatus为终止状态(
退出代码)
BOOL TerminateThread(HANDLE hThread, DWORD dwStatus);
ExitThread()将正常终止线程,一个线程函数如果调用ExitThread()等价于使用
return返回,系统将该线程从调度队列中删除,并复位堆栈。实际上,线程函数运
行到结尾或使用return时,系统自动调用ExitThread()。
一个线程通过调用TerminateThread()可以强迫另一个线程立刻停止,它第一个
参数指出了将被终止的线程的句柄。需要注意的是,它不释放线程占用的资源,不
做清除内存的操作,这样可能会引起系统不稳定。
如果多线程程序中使用了标准C库函数,并用CreateThread()和ExitThread(),
则会导致内存泄漏。解决这个问题的方法是用C运行库(run-time library)函数来启
动和终止线程,而不用WIN32 API定义的CreateThread()和ExitThread()。在C运行
库函数中,它们的替代函数分别是_beginthreadex()和_exitthreadex(),需要的头文件
是_process.h。在VC6.0下,还需在Project->Settings->C/C++->Code Generation中选
择Multithreaded Runtime Library。当然,也可以通过避免使用C标准库函数的方法
来解决上述问题,WIN32提供了一些C标准库函数的替代函数,例如,可用wsprintf()
和lstrlen()来代替sprintf()和strlen()。这样,使用CreateThread()和ExitThread()不会
出现问题。

2.3 挂起和继续执行线程
一个线程可以挂起和继续执行另外一个线程,使用的函数是:
DWORD SuspendThread(HANDLE hThread);
DWORD ResumeThread(HANDLE hThread);
每个线程都有一个挂起次数(suspend counts)与它相连,每次调用
SuspendThread(),挂起次数就加1,调用ResumeThread()时减1。挂起次数为0时线
程没有被挂起,可以继续执行,这意味着这两个调用次数应相等。
线程可以挂起本身,但不能唤醒本身。若要把自己挂起一段指定时间后继续运
行,可用Sleep()函数:
VOID Sleep(DWORD Duration);
参数Duration以毫秒为单位,指定挂起时间,到达时间后线程自动继续运行。

2.4 关于优先级的操作
在程序中,可以获取和更改进程和线程的优先级。对于进程的优先级类有如下
函数:
DWORD GetPriorityClass(HANDLE hProcess); // hProcess为进程句柄
BOOL SetPriorityClass(HANEL hProcess, DWORD dwPriority);
对线程的优先级有这两个函数:
int GetThreadPriority(HANDLE hThread); //hThread为线程句柄
BOOL SetThreadPriority(HANDLE hThread, int nPriority);
关于进程和线程的优先级见前面的讨论。

2.5 同步对象的使用
同步是允许用户控制两个或多个线程同时执行的机制。从前面的分析可看出,
Windows98的每种同步对象可以处于两种状态:信号状态(signal state)或非信号状
态(nonsignal state)。当一个线程与某个对象相关联时,若该对象处于非信号状态,
则要等到其变成信号状态线程才能继续执行。
为了实现等待一个对象达到信号状态
这一功能,WIN32 API提供了等待命令WaitForSingleObject和
WaitForMutipleObjects。这两个等待命令可以用来等待下列WIN32核心对象:进程、
线程、互斥量、信号量、事件、可等待定时器、控制台输入(Console input)和文件
变化通知(File changte notification)等,但不包括临界区。
其原型如下:
DWORD WaitForSingleObject(
HANDLE hObject, // 要等待的对象句柄
DWORD dwMilliseconds); // 最大等待时间(毫秒)
DWORD WaitforMultipleObjects(
DWORD dwNumObjects, // 要等待的对象数
LPHANDLE lpHandles, // 对象句柄数组
BOOL bWaitAll, // 是否等待所有对象都
有信号才返回
DWORD dwMilliseconds); // 最大等待时间
这两个函数分别用来等待单个对象和若干个对象。如果在指定时间内对象达到
信号状态则返回WAIT_OBJECT_0,超时返回WAIT_TIMEOUT,出错返回
WAIT_FAILED。对于互斥量、信号量和自动重置(auto-reset)事件对象,等待成功
时将它们改成非信号状态(信号量计数器减1),以实现对象的互斥访问。

2.5.1 信号量(Semaphore)
信号量允许多个线程在给定限制条件下访问共享资源。用下面的函数创建一个
信号量:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSecAttr, // 安全属性指针,Win9
8忽略
LONG InitialCoutn,
// 初始信号量数目
LONG lMaxCount, // 允
许的最大信号量数目
LPCTSTR lpszSemName); // 信号量对象
名指针
当信号量数目大于0时,就处于信号状态。若lpszSemName参数为NULL时,
信号量局限于一个进程的线程中;若给予一个字符串对象名,则其它进程也可以使
用该信号量。别的进程中可以在调用CreateSemaphore()和OpenSemaphore()时把这个
字符串名作参数,以打开该信号量,这样可以实现多个进程间的同步。关于同步对
象名,在互斥量、事件和可等待定时器也有相似情况,即若被指定对象名,则可以
在多个进程间实现同步。
信号量创建后,返回对象句柄。线程在访问共享资源前可以通过调用
WaitForSingleObject()和WaitForMultipleObjects()来等待一个信号量。当完成同步任
务时,线程应用ReleaseSemaphore()来释放信号量:
BOOL ReleaseSemaphore(HANDLE hSema, LONG lReleaseCount, LPLONG
lpPrevious);

2.5.2 互斥量(Mutex Semaphore)
互斥量可被用来实现一个或多个进程的线程间的共享资源的互斥访问。互斥量
的创建函数如下:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpsa, // 安全属性指针,Win98忽略
BOOL bInitialOwner, // TR
UE表示线程将拥有该信号量
LPCTSTR lpName); // 对
象名,意义同信号量名
创建互斥量后,在别的进程中可以用相同的互斥量名调用CreateMutex()和
OpenMutex()来打开该对象,这样互斥量就可用于多个进程中。
与信号量一样,线程通过WaitForSingleObject()和WaitForMultipleObject()来等
待一个互斥量,用ReleaseMutex()来释放一个互斥量。

2.5.3 事件(Event)对象
事件对象用来向线程发信号以表示某一操作以经完成。创建事件的API函数如
下:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpsa, // 安全属性指针,Win98忽略
BOOL bManualRest, // 是
否手动重置(manual-reset)
BOOL bInitialState, // 初
始状态是否为信号状态
LPCTSTR lpName); // 对象名字符
串指针,意义同信号量名
创建成功后,要等待事件的线程简单调用WaitForSingleObject()和
WaitForMultipleObjects()即可。
有两种事件对象:自动重置(auto-reset)事件和手动重置(manual-reset)事件,
这由CreateEvent()的第二个参数指定。对于自动重置事件,WaitForSingleObject()和
WaitForMultipleObjects()会等待事件到信号状态,随后又自动将其重置为非信号状
态,这样保证了等待此事件的线程中只有一个会被唤醒。而手动重置事件需要用户
调用ResetEvent()才会重置事件。可能有若干个线程在等待同一事件,这样当事件变
为信号状态时,所有等待线程都可以运行了。
SetEvent()函数用来把事件对象设置成信号状态,ResetEvent()把事件对象重置成
非信号状态,两者均需事件对象句柄作参数。显然,对于自动重置事件,后者是多
余的。

2.5.4 临界区(Critical Section)
临界区与互斥量相似,只是它不能在进程之间共享,只能在同一进程的多个线
程之间控制共享资源的访问。但相比之下,临界区的效率较高。使用前要先定义一
个临界区对象:
CRITICAL_SECTION cs;
然后初始化,调用下面函数(参数为空对象的指针):
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpcs);
进入和离开临界区分别调用函数:
VOID EnterCriticalSection(LPCRITICAL_SECTION lpcs);
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpcs);
删除临界区对象用:
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpcs);
需要注意的是,引用临界区对象用指针,不同于别的同步对象那样用句柄。

Critial Section简单易用。但是这是以其灵活性对代价的。看下面这个线程函数:
void Swap(Object* first,Objct* second){
 EnterCriticalSection(&first->cs);  
 EnterCriticalSection(&second->cs);
//do swap  
 LeaveCriticalSection(&second->cs);
 LeaveCriticalSection(&first->cs);
}

假如两个线程几乎在同一时间内调用Swap函数。

void Swap(first,second)
void Swap (second,first)

当线程1进入第一个Critical Section时,线程发生调度。然后线程2也进入第一个Critical Section。这时就发生死锁。
这是我们想到用WaitForMultiObject函数,要么全获得,要么一个都不要。但又发现Critical Section没有Handle.
无处入手。这时我们可以采用mutrex 取而代之。

下面是一个简单的对比表:
 
   Critical_Section                                           Mutrex

   锁住Critical_Section的时间比锁住                     Mutrex有Handle,可以有名字。
   Mutrex快很多。Critical_Section在用户状执行       Mutrex可以跨进程。
   Mutrex在核心态。
   
   InitializeCritialSection                                  CreateMutrex
                                                                   OpenMutrex

   EnterCritialSection                                 
                                                                    WaitForSingleObject
                                                                    WaitForMultipleObject
                                                                    MsgWaitForMultipleObject
  LeaveCritialSection                                        ReleaseMutrex
  DeleteCritialSection                                       CloseHandle.

WIDOW API对Mutrex的操作主要有以下几个函数
 1.Handle CreateMutrex(LPSECUEIRY_ATTRIBUTES,BOOL initOwner,LPSTR Name); --创建一个互斥量,如果让当前线程锁住该Mutrex。
   initOwner为TRUE,name设定Mutrex的名字。
 2.HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle,LPCTSTR lpName); --打开一个Mutex
 3.WaitForSingleObject,WaitForMultipleObject,MsgWaitForMultipleObject 锁定mutrex
 4.ReleaseMutex(Handle mutrex) 释放Mutex,线程结束同样会释放Mutex。
 
  上面的例子换成Mutex,消除了死锁的可能性:

 void Swap(Object* first,Objct* second){
 HANDLE hs[2] = HANDLE[]{first->mutrex,second->mutrex};  
 MsgWaitForMultipleObject(2,hs,true,INFINITE);
  //do swap 
 ReleaseMutex(hs[0]);
 ReleaseMutex(hs[1]);

2.5.5 可等待定时器(Waitable Timer)
可等待定时器可以自动化后台任务,让其等待到预定时间再运行。首先需要用
CreateWaitableTimer()来创建一个可等待定时器,再用SetWaitableTimer()来设置相应
参数。线程可以用WaitForSingleObject()来等待此定时器,也可先编写一个定时器函
数,再在SetWaitableTimer()中将其设定为定时器计数完成时将自动调用的函数。

2.6 创建一个新的进程
上面讨论的都是基于多线程的多任务,但有时候用户可能想创建一些有自己独
立内存空间并独立于原进程的新进程,即使用基于多进程的多任务。这样,程序将
启动另一个程序的执行,而不是启动同一进程的另一线程。WIN32 API提供了函数
CreateProcess()来实现这一功能,并淘汰了WIN 16的WinExec()。

2.7 一个用WIN32 API进行多线程编程的实例——生产者与消费者问题
生产者与消费者问题是一个比较经典的同步例子,现实生活中也有许多类似的
实例。设有多个生产者和消费者共享一缓冲区buffer[0...BUFFER_SIZE-1],生产者
生产产品并放入其中,消费者从中取出产品消费。在这个PCDEMO程序中,我们
为每个生产者创建一个Producer线程,为每个消费者创建一个Consumer 线程,模
拟它们的活动。把buffer[]看成一环行队列,下标in指示当前可供放产品的位置,out
指示当前可取产品的位置,in等于out表示buffer中无产品,out==(in+BUFFER_SIZE
–1)%BUFFER_SIZE时表示buffer已满,也就是说buffer最多可放BUFFER_SIZE-1
个产品。要使生产者和消费者协调进行,必须注意以下同步规则:
(1)buffer已满时不能再放入产品,所有生产者线程必须等待。为此引入一个
信号量hSemaProducer,初始资源数和最大资源数均为BUFFER_SIZE-1,Producer
存取buffer[in]前要等待hSemaProducer;
(2)buffer已空时不能再取产品,所有消费者现成必须等待。为此引入一个信
号量hSemaConsumer,初始资源数为0;
(3)下面的语句表示往buffer中放一产品:
buffer[in] = PRODUCT_FLAG;
in = (in + 1) % BUFFER_SIZE;
若有多个生产者线程,则每次只允许一个访问buffer(即执行上述两句),否则这
两个语句可能被打断,而另一个生产者线程也执行buffer[in]=PRODUCT_FLAG,造
成buffer[in]放了两件产品。为此引入一互斥量hMutexProducer以限制生产者线程的
互斥执行。
(4)用这样来表示取一产品:
buffer[out] = NOTPRODUCT_FLAG;
out = (out + 1) % BUFFER_SIZE;
同样的原理,为限制多个消费者线程同时存取buffer,引入一互斥量
hMutexConsumer。
这个例子程序名为PCDEMO,执行时出现一窗口,它用来显示当前有多少个线
程,其中生产者和消费者线程各有多少,缓冲区中存放了多少件产品(包括每个缓
冲区单元的状态),当前是否有生产者或消费者线程在活动。程序中定义了一个菜单
项Demo,包括四个子菜单Add a Producer、Add a Consumer、Delete All Threads、Exit,
ID分别为IDM_ADDPRODUCER、IDM_ADDCONSUMER、IDM_DELETEALL 、
IDM_EXIT,同时定义了两个加速键F2、F3对应前两个子菜单项。
为了实现前面所述的同步功能,PCDEMO定义了四个同步对象句柄:
// 4 handles of objects for synchronize to access shared variable in, out.
HANDLE hMutexProducer, hMutexConsumer, hSemaProducer, hSemaConsumer;
在窗口消息函数WindowFunc()中响应WM_CREATE消息时创建这些同步对象,
并初始化buffer[]:
switch (message) {
case WM_CREATE:
// create mutex and semaphore
hMutexProducer = CreateMutex(NULL, FALSE, NULL);
hMutexConsumer = CreateMutex(NULL, FALSE, NULL);
hSemaProducer = CreateSemaphore(NULL,
BUFFER_SIZE - 1, BUFFER_SIZE - 1, NULL);
hSemaConsumer = CreateSemaphore(NULL,
0, BUFFER_SIZE - 1, NULL);
// initialize buffer[], in, out
for (i = 0; i < BUFFER_SIZE; i++)
buffer[i] = NOTPRODUCT_FLAG;
in = out = 0;
...
选择菜单项Add a Producer时将创建一个生产者线程:
case WM_COMMAND:
switch (LOWORD(wParam)) {
case IDM_ADDPRODUCER: // create a producer thread
if (numProducer >= MAX_PRODUCER)
break;
CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)Producer,
(LPVOID)hwnd, 0, &ProducerID[numProducer]);
...
生产者线程函数如下:
DWORD WINAPI Producer(LPVOID param)
{
while (! bDone) {
// wait for mutex and semaphore to keep synchronize
WaitForSingleObject(hSemaProducer, INFINITE);
WaitForSingleObject(hMutexProducer, INFINITE);

// change the thread state and dispaly in window
bIsOneProducerActive = true;
InvalidateRect((HWND)param, NULL, 1);

// produce for a random time
Sleep(GetRandomTime());
// access the shared resources
buffer[in] = PRODUCT_FLAG;
in = (in + 1) % BUFFER_SIZE;

// finish producing
bIsOneProducerActive = false;
InvalidateRect((HWND)param, NULL, 1);

ReleaseMutex(hMutexProducer);
ReleaseSemaphore(hSemaConsumer, 1, NULL);

// sleep for a while before next produce action
Sleep(GetRandomTime());
}

// thread will exit
numProducer--;
nTotalThreads--;
InvalidateRect((HWND)param, NULL, 1);

return 0;
}
该函数循环中开始时要等待生产者信号量和生产者互斥量,结束时要释放互斥
量和一个消费者信号量,此时如有等待的消费者,此消费者线程将获准通行。
同样,选择菜单项Add a Cosumer将创建一个消费者线程,消费者线程Consumer
的代码实现原理与生产者相似。以上是该程序算法的主要部分,详见附录PCDEMO
程序。它执行时的界面如图1所示。

图1:PCDEM程序执行效果


3. 基于MFC的多线程编程
直接利用WIN32 API接口函数库编写Windows应用程序是一件非常复杂的事,
为此Microsoft开发出了封装Windows数据结构和API函数的C++类库——MFC,
为广大开发者提供了一个方便的面向对象的Windows编程接口。用户可以利用VC6.0
提供的MFC类库编写多线程应用程序,原理与用WIN32 API一致。需要说明的是,
用MFC编写的程序中仍然可以调用WIN32 API函数,一般在要调用的函数名前加
“::”。

3.1 MFC类库对多线程的支持
MFC支持两种类型的线程:
工作者线程(Worker Thread,也可叫辅助线程)和
用户界面线程(User Interface Thread)
。工作者线程没有消息机制,常用来实现像计
算、后台打印这样的任务,不需要与用户交互;MFC为用户界面线程提供消息机制,
用来处理用户输入。这两种线程类都是从CWinThread类派生而来的,不同的是,
对于工作者线程不需要显式的创建CWinThread对象,而由AfxBeginThread()自动创
建。
在MFC应用程序中,主线程是从CWinApp类派生来的。事实上,CWinApp就
是用户界面线程的一个典型实例,它是从CWinThread派生而来。在MFC中,由如
下派生关系:CObject->CCmdTarget->CWinThread->CWinApp。

3.2 工作者线程(Worker Thread)的创建
要创建一个线程需要调用AfxBeginThread(),它有两个版本,分别对应工作者
线程和用户界面线程。创建工作者线程的AfxBeginThread()原型如下:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc, // 线程函数地址
LPVOID pParam, // 传
给线程函数的参数
int nPriority = THREAD_PRIORITY_NORMAL // 线程优先级
UINT nStackSize = 0, // 最
大堆栈大小
DWORD dwCreateFlags = 0, // 启
动标志
LPSECURITY_ATTRIBUTES lpSecAttr = NULL); // 安全属性指针
AfxBeginThread()的各参数含义与WIN32 API的CreateThread()基本相同。工作
者线程的线程函数应具有下列形式:
UINT MyThreadProc(LPVOID pParam);
AfxBeginThread()将为新线程自动建立一个CWinThread对象,调用其成员函数
CreateThread开始该线程,并返回线程对象指针。一般线程函数运行到终点线程即
结束,也可调用AfxEndThread()来终止线程。

3.3 创建用户界面线程(User Interface Thread)
首先,从CWinThread类派生出自己的线程类(可用VC6中的ClassWizard),
必须重载其InitInstance(),用这个函数为线程作初始化,如创建一个窗口。另外要
确保用宏DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE对该类进行声明
和实现,因为创建线程时要动态地创建类的对象。
接下来就可以用AfxBeginThread()创建并启动一个用户界面线程了,其原型如
下:
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass, // 线程的运行类
int nPriority = THREAD_PRIORITY_NORMAL, // 线程优先级
UINT nStackSize = 0, // 最大堆栈大小
DWORD dwCreateFlags = 0, // 启动标志
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); // 安全属性指针
例如,已经从CWinThread派生了一个线程类MyThreadClass,就可以这样创建
线程:
AfxBeginThread((RUNTIME_CLASS(MyThreadClass));
还有第二种方法可以创建一个用户界面线程,即先通过线程类构造函数创建一
个线程对象,再调用CWinThread::CreateThread()来启动线程。
在用户界面线程中可以处理消息,这可通过重载CWinThread::Run()实现。用户
可以用一个特殊的消息映射宏ON_THREAD_MESSAGE处理直接送给线程的消息,
通过重载PreTranslateMessage()在消息派遣前重新解释消息,用
CWinThread::PostThreadMessage()直接把消息送给线程。

3.4 MFC线程同步类和同步访问类的使用
MFC封装了线程同步操作,提供了一组同步类和同步访问类来解决同步问题。
同步类有:CSyncObject,CSemaphore,CMutex,CCriticalSection,CEvent。CSyncObject
是其余四个类的基类,不直接使用,后四个分别对应前面讲的WIN32 API同步对象:
信号量、互斥量、临界区、事件对象。两个同步访问类是CSingleLock和CMultiLock。
同步访问类要和同步对象配合使用。使用同步类时程序中要包含头文件“afxmt.h”。

3.4.1 四种线程同步类的使用
四种同步对象的使用方法基本上是一致的,很多情况下不需要同步访问类
CSingleLock和CMultipleLock,直接调用它们自身的成员函数Lock()和Unlock()即
可。
需要线程同步时,首先用构造函数创建同步对象,例如可以这样创建一个信号
量对象:
CSemaphore semaObj;
接下来,在访问共享资源或进入需要同步的操作之前,调用Lock()等待对象变
成信号状态(signal state),如:
semaObj.Lock();
操作完成后调用Unlock()释放对象,如:
semaObj.Unlock();
其它三种同步对象CMutex、CCriticalSection和CEvent的使用方法与CSemaphore
类似。程序中也可以调用WIN32 API函数::WaitForSingleObject()
和::WaitForMultipleObjects()来等待对象。

3.4.2 同步访问类:CSingleLock和CMultipleLock
可以不用同步对象的Lock()和Unlock()而使用类CSingleLock和CMultipleLock
来进行实际上的访问控制,其方法是先创建一个CSingleLock或CMultipleLock对象,
然后调用其Lock函数等待对象的访问权,访问结束时调用其Unlock函数(在析构
函数中也会自动调用Unlock)。例如,假设已创建了一信号量对象semaObj,就可以
这样创建CSingleObject对象:
CSingleObject singleLock(&semaObj); // 参数为希望控制的同步对象的
指针
等待信号量对象用:
singleLock.Lock();
释放用:
singleLock.Unlock();
CMultipleLock类是用来进行等待几种不同资源或事件的操作的。事实上,这两
种类操作内部分别要调用::WaiForSingleObject()和::WaitForMultipleObjects()。

0 0