MFC多线程及线程同步.docx
《MFC多线程及线程同步.docx》由会员分享,可在线阅读,更多相关《MFC多线程及线程同步.docx(16页珍藏版)》请在冰豆网上搜索。
MFC多线程及线程同步
MFC多线程及线程同步
一、MFC对多线程编程的支持
MFC中有两类线程,分别称之为工作者线程和用户界面线程。
二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。
工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。
用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。
但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。
在MFC中,一般用全局函数AfxBeginThread()来创建并初始化一个线程的运行,该函数有两种重载形式,分别用于创建工作者线程和用户界面线程。
两种重载函数原型和参数分别说明如下:
(1)CWinThread*AfxBeginThread(
AFX_THREADPROCpfnThreadProc,
LPVOIDpParam,
intnPriority=THREAD_PRIORITY_NORMAL,
UNTnStackSize=0,
DWORDdwCreateFlags=0,
LPSECURITY_ATTRIBUTESlpSecurityAttrs=NULL
);//用于创建工作者线程
PfnThreadProc:
指向工作者线程的执行函数的指针,线程函数原型必须声明如下:
UINTExecutingFunction(LPVOIDpParam);
请注意,ExecutingFunction()应返回一个UINT类型的值,用以指明该函数结束的原因。
一般情况下,返回0表明执行成功。
∙pParam:
一个32位参数,执行函数将用某种方式解释该值。
它可以是数值,或是指向一个结构的指针,甚至可以被忽略;
∙nPriority:
线程的优先级。
如果为0,则线程与其父线程具有相同的优先级;
∙nStackSize:
线程为自己分配堆栈的大小,其单位为字节。
如果nStackSize被设为0,则线程的堆栈被设置成与父线程堆栈相同大小;
∙dwCreateFlags:
如果为0,则线程在创建后立刻开始执行。
如果为CREATE_SUSPEND,则线程在创建后立刻被挂起;
∙lpSecurityAttrs:
线程的安全属性指针,一般为NULL;
(2)CWinThread*AfxBeginThread(
CRuntimeClass*pThreadClass,
intnPriority=THREAD_PRIORITY_NORMAL,
UNTnStackSize=0,
DWORDdwCreateFlags=0,
LPSECURITY_ATTRIBUTESlpSecurityAttrs=NULL
);
pThreadClass是指向CWinThread的一个导出类的运行时类对象的指针,该导出类定义了被创建的用户界面线程的启动、退出等;其它参数的意义同形式1。
使用函数的这个原型生成的线程也有消息机制,在以后的例子中我们将发现同主线程的机制几乎一样。
下面我们对CWinThread类的数据成员及常用函数进行简要说明。
∙m_hThread:
当前线程的句柄;
∙m_nThreadID:
当前线程的ID;
∙m_pMainWnd:
指向应用程序主窗口的指针
BOOLCWinThread:
:
CreateThread(DWORDdwCreateFlags=0,UINTnStackSize=0,LPSECURITY_ATTRIBUTESlpSecurityAttrs=NULL);
该函数中的dwCreateFlags、nStackSize、lpSecurityAttrs参数和API函数CreateThread中的对应参数有相同含义,该函数执行成功,返回非0值,否则返回0。
一般情况下,调用AfxBeginThread()来一次性地创建并启动一个线程,但是也可以通过两步法来创建线程:
首先创建CWinThread类的一个对象,然后调用该对象的成员函数CreateThread()来启动该线程。
virtualBOOLCWinThread:
:
InitInstance();
重载该函数以控制用户界面线程实例的初始化。
初始化成功则返回非0值,否则返回0。
用户界面线程经常重载该函数,工作者线程一般不使用InitInstance()。
virtualintCWinThread:
:
ExitInstance();
在线程终结前重载该函数进行一些必要的清理工作。
该函数返回线程的退出码,0表示执行成功,非0值用来标识各种错误。
同InitInstance()成员函数一样,该函数也只适用于用户界面线程。
二、MFC中线程同步
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。
更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。
正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。
例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。
如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。
象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。
用户模式中线程的同步方法主要有原子访问和临界区等方法。
其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。
由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
1.临界区
临界区(CriticalSection)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。
如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。
临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。
所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。
否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
代码
CRITICAL_SECTION g_cs; // 临界区结构对象
char g_cArray[10]; // 共享资源
UINT ThreadProc10(LPVOID pParam)
{
EnterCriticalSection(&g_cs); // 进入临界区
for (int i = 0; i < 10; i++) // 对共享资源进行写入操作
{
g_cArray[i] = 'a';
Sleep
(1);
}
LeaveCriticalSection(&g_cs); // 离开临界区
return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
EnterCriticalSection(&g_cs);
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep
(1);
}
LeaveCriticalSection(&g_cs);
return 0;
}
……
void CSample08View:
:
OnCriticalSection()
{
InitializeCriticalSection(&g_cs); // 初始化临界区
AfxBeginThread(ThreadProc10, NULL); // 启动线程
AfxBeginThread(ThreadProc11, NULL);
Sleep(300);
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
在使用临界区时,一般不允许其运行时间过长,只要进入临界区的线程还没有离开,其他所有试图进入此临界区的线程都会被挂起而进入到等待状态,并会在一定程度上影响。
程序的运行性能。
尤其需要注意的是不要将等待用户输入或是其他一些外界干预的操作包含到临界区。
如果进入了临界区却一直没有释放,同样也会引起其他线程的长时间等待。
换句话说,在执行了EnterCriticalSection()语句进入临界区后无论发生什么,必须确保与之匹配的LeaveCriticalSection()都能够被执行到。
可以通过添加结构化异常处理代码来确保LeaveCriticalSection()语句的执行。
虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
MFC为临界区提供有一个CCriticalSection类,使用该类进行线程同步处理是非常简单的,只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。
对于上述代码,可通过CCriticalSection类将其改写如下:
代码
CCriticalSection g_clsCriticalSection; // MFC临界区类对象
char g_cArray[10]; // 共享资源
UINT ThreadProc20(LPVOID pParam)
{
g_clsCriticalSection.Lock(); // 进入临界区
for (int i = 0; i < 10; i++) // 对共享资源进行写入操作
{
g_cArray[i] = 'a';
Sleep
(1);
}
g_clsCriticalSection.Unlock(); // 离开临界区
return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
g_clsCriticalSection.Lock();
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep
(1);
}
g_clsCriticalSection.Unlock();
return 0;
}
……
void CSample08View:
:
OnCriticalSectionMfc()
{
AfxBeginThread(ThreadProc20, NULL);
AfxBeginThread(ThreadProc21, NULL);
Sleep(300);
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
2.事件内核对象
在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。
对于前面那段使用临界区保持线程同步的代码可用事件对象的线程同步方法改写如下:
代码
HANDLE hEvent = NULL; // 事件句柄
char g_cArray[10]; // 共享资源
UINT ThreadProc12(LPVOID pParam)
{
WaitForSingleObject(hEvent, INFINITE); // 等待事件置位
for (int i = 0; i < 10; i++)
{
g_cArray[i] = 'a';
Sleep
(1);
}
SetEvent(hEvent); // 处理完成后即将事件对象置位
return 0;
}
UINT ThreadProc13(LPVOID pParam)
{
WaitForSingleObject(hEvent, INFINITE);
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = 'b';
Sleep
(1);
}
SetEvent(hEvent);
return 0;
}
……
void CSample08View:
:
OnEvent()
{
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); // 创建事件
SetEvent(hEvent); // 事件置位
AfxBeginThread(ThreadProc12, NULL); // 启动线程
AfxBeginThread(ThreadProc13, NULL);
Sleep(300);
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
在创建线程前,首先创建一个可以自动复位的事件内核对象hEvent,而线程函数则通过WaitForSingleObject()等待函数无限等待hEvent的置位,只有在事件置位时WaitForSingleObject()才会返回,被保护的代码将得以执行。
对于以自动复位方式创建的事件对象,在其置位后一被WaitForSingleObject()等待到就会立即复位,也就是说在执行ThreadProc12()中的受保护代码时,事件对象已经是复位状态的,这时即使有ThreadProc13()对CPU的抢占,也会由于WaitForSingleObject()没有hEvent的置位而不能继续执行,也就没有可能破坏受保护的共享资源。
在ThreadProc12()中的处理完成后可以通过SetEvent()对hEvent的置位而允许ThreadProc13()对共享资源g_cArray的处理。
这里SetEvent()所起的作用可以看作是对某项特定任务完成的通知。
使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。
可以通过OpenEvent()函数获取得到,其函数原型为:
HANDLE OpenEvent(
DWORD dwDesiredAccess, // 访问标志
BOOL bInheritHandle, // 继承标志
LPCTSTR lpName // 指向事件对象名的指针
);
如果事件对象已创建(在创建事件时需要指定事件名),函数将返回指定事件的句柄。
对于那些在创建事件时没有指定事件名的事件内核对象,可以通过使用内核对象的继承性或是调用DuplicateHandle()函数来调用CreateEvent()以获得对指定事件对象的访问权。
在获取到访问权后所进行的同步操作与在同一个进程中所进行的线程同步操作是一样的。
如果需要在一个线程中等待多个事件,则用WaitForMultipleObjects()来等待。
WaitForMultipleObjects()与WaitForSingleObject()类似,同时监视位于句柄数组中的所有句柄。
这些被监视对象的句柄享有平等的优先权,任何一个句柄都不可能比其他句柄具有更高的优先权。
WaitForMultipleObjects()的函数原型为:
DWORD WaitForMultipleObjects(
DWORD nCount, // 等待句柄数
CONST HANDLE *lpHandles, // 句柄数组首地址
BOOL fWaitAll, // 等待标志
DWORD dwMilliseconds // 等待时间间隔
);
参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。
fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。
dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。
如果等待超时,函数将返回WAIT_TIMEOUT。
如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某个值,则说明所有指定对象的状态均为已通知状态(当fWaitAll为TRUE时)或是用以减去WAIT_OBJECT_0而得到发生通知的对象的索引(当fWaitAll为FALSE时)。
如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知,且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用以减去WAIT_OBJECT_0表示一个等待正常结束的互斥对象的索引(当fWaitAll为FALSE时)。
下面给出的代码主要展示了对WaitForMultipleObjects()函数的使用。
通过对两个事件内核对象的等待来控制线程任务的执行与中途退出:
代码
HANDLE hEvents[2]; // 存放事件句柄的数组
UINT ThreadProc14(LPVOID pParam)
{
DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE); // 等待开启事件
if (dwRet1 == WAIT_OBJECT_0) // 如果开启事件到达则线程开始执行任务
{
AfxMessageBox("线程开始工作!
");
while (true)
{
for (int i = 0; i < 10000; i++);
DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0); // 在任务处理过程中等待结束事件
if (dwRet2 == WAIT_OBJECT_0 + 1) // 如果结束事件置位则立即终止任务的执行
break;
}
}
AfxMessageBox("线程退出!
");
return 0;
}
……
void CSample08View:
:
OnStartEvent()
{
for (int i = 0; i < 2; i++) // 创建线程
hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
AfxBeginThread(ThreadProc14, NULL); // 开启线程
SetEvent(hEvents[0]); // 设置事件0(开启事件)
}
void CSample08View:
:
OnEndevent()
{
SetEvent(hEvents[1]); // 设置事件1(结束事件)
}
MFC为事件相关处理也提供了一个CEvent类,共包含有除构造函数外的4个成员函数PulseEvent()、ResetEvent()、SetEvent()和UnLock()。
在功能上分别相当与Win32API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函数。
而构造函数则履行了原CreateEvent()函数创建事件对象的职责,其函数原型为:
CEvent(BOOLbInitiallyOwn=FALSE,BOOLbManualReset=FALSE,LPCTSTRlpszName=NULL,LPSECURITY_ATTRIBUTESlpsaAttribute=NULL);
3.信号量内核对象
信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。
一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。
但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。
线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。
在任何时候当前可用资源计数决不可能大于最大资源计数。
使用信号量内核对象进行线程同步主要会用到CreateSemaphore()、OpenSemaphore()、ReleaseSemaphore()、WaitForSingleObject()和WaitForMultipleObjects()