Visual C++ 线程同步技术剖析

xwgg

贡献于2014-04-28

字数:0 关键词: C/C++开发 C/C++

Visual C++线程同步技术剖析 (转载 ) 作者:中国电波传播研究所 郎锐■来自:yesky 摘要: 多线程同步技术是计算机软件开发 的重要技术,本文对多线程的各种同步技 术的原理和实现进 行了初步探讨。 关键 词 : VC++6.0; 线程同步;临界区;事件;互斥;信号量; 正文 使线程同步 在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的 操作。更多的情况是一些线程进行某些处理操作,而其他的线程必须对 其处理结果 进行了解。正常情况下对这种处 理结果的了解应当在其处理任务完成后进行。 如果不采取适当的措施,其他线程往往会在线程处理任务结 束前就去访问处 理 结果,这就很有可能得到有关处 理结果的错误 了解。例如,多个线程同时访问 同一 个全局变量,如果都是读取操作,则不会出现问题 。如果一个线程负责 改变此变量 的值,而其他线程负责 同时读 取变量内容,则不能保证读 取到的数据是经过 写线程 修改后的。 为了确保读线 程读取到的是经过 修改的变量,就必须在向变量写入数据时禁止 其他线程对其的任何访问 ,直至赋值过 程结束后再解除对其他线程的访问 限制。象 这种 保证线 程能了解其他线程任务处 理结束后的处理结果而采取的保护措施即为 线程同步。 线程同步是一个非常大的话题 ,包括方方面面的内容。从大的方面讲,线程的同 步可分用户模式的线程同步和内核对象的线程同步两大类。用户模式中线程的同步 方法主要有原子访问 和临界区等方法。其特点是同步速度特别快,适合于对线 程运 行速度有严格要求的场合。 内核对象的线程同步则 主要由事件、等待定时器、信号量以及信号灯等内核对象 构成。由于这种 同步机制使用了内核对象,使用时必须将线程从用户模式切换到内 核模式,而这种转换 一般要耗费近千个 CPU 周期,因此同步速度较慢,但在适用性 上却要远优 于用户模式的线程同步方式。 临界区 临界区(Critical Section)是一段独占对某些共享资源访问 的代码,在任意时刻只 允许一个线程对共享资源进行访问 。如果有多个线程试图 同时访问临 界区,那么在 有一个线程进入后其他所有试图访问 此临界区的线程将被挂起,并一直持续到进入 临界区的线程离开。临界区在被释放后,其他线程可以继续抢 占,并以此达到用原 子方式操作共享资源的目的。 临界区在使用时以CRITICAL_SECTION 结构对象保护共享资源,并分别用 EnterCriticalSection()和 LeaveCriticalSection()函数去标识 和释放一个临界区。所 用到的 CRITICAL_SECTION 结构对象必须经过 InitializeCriticalSection()的初始化 后才能使用,而且必须确保所有线程中的任何试图访问 此共享资源的代码都处在此 临界区的保护之下。否则临 界区将不会起到应有的作用,共享资源依然有被破坏的 可能。 图1 使用临界区保持线程同步 下面通过一段代码展示了临界区在保护多线程访问 的共享资源中的作用。通过 两个线程来分别对 全局变量g_cArray[10]进行写入操作,用临界区结构对象g_cs 来 保持线程的同步,并在开启线程前对其进行初始化。为了使实验 效果更加明显,体 现出临界区的作用,在线程函数对共享资源g_cArray [10]的写入时,以 Sleep()函数 延迟1毫秒,使其他线程同其抢占CPU 的可能性增大。如果不使用临界区对其进行 保护,则共享资源数据将被破坏(参见图 1(a)所示计算结果),而使用临界区对线 程 保持同步后则可以得到正确的结果(参见图 1(b)所示计算结果 )。代码实现 清单附下 : // 临界区结构对象 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); // 报告计算结果 在使用临界区时,一般不允许其运行时间过长 ,只要进入临界区的线程还没有离 开,其他所有试图进 入此临界区的线程都会被挂起而进入到等待状态,并会在一定 程度上影响程序的运行性能。尤其需要注意的是不要将等待用户输 入或是其他一些 外界干预的操作包含到临界区。如果进入了临界区却一直没有释放,同样也会引起 其他线程的长时间 等待。换句话说 ,在执行了 EnterCriticalSection()语句进入临界 区后无论发 生什么,必须确保与之匹配的 LeaveCriticalSection()都能够被执行 到 。 可以通过添加结构化异常处理代码来确保 LeaveCriticalSection ()语句的执行。虽 然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个 进程中的线程。 MFC 为临 界区提供有一个 CCriticalSection 类,使用该类进 行线程同步处 理是非 常简单 的,只需在线程函数中用 CCriticalSection 类成员函数 Lock()和 UnLock( ) 标定出被保护代码片段即可。对于上述代码,可通过CCriticalSection 类将其改写如 下: CString sResult = CString(g_cArray); AfxMessageBox(sResult); } // MFC 临界区类对 象 CCriticalSection g_clsCriticalSection; // 共享资源 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); } // 事件句柄 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,而线程函数 则通过WaitForSingleObject()等待函数无限等待 hEvent 的置位,只有在事件置位 时WaitForSingleObject()才会返回,被保护的代码将得以执行。对于以自动复 位方 式创建的事件对象,在其置位后一被 WaitForSingleObject()等待到就会立即复位 , 也就是说在执行ThreadProc12()中的受保护代码时 ,事件对象已经是复位状态的 , 这时 即使有 ThreadProc13()对CPU 的抢占,也会由于 WaitForSingleObject()没有 hEvent 的置位而不能继续执 行,也就没有可能破坏受保护的共享资源。在 ThreadProc12()中的处理完成后可以通过SetEvent()对hEvent 的置位而允许 ThreadProc13()对共享资源g_cArray 的处理。这里SetEvent()所起的作用可以看作 是对某项特定任务完成的通知。 使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进 程外 的线程进行同步,其前提是得到对此事件对象的访问权 。可以通过OpenEvent()函 数获取得到,其函数原型为: hEvent = CreateEvent(NULL, FALSE,FALSE,NULL); // 事件置位 SetEvent(hEvent); // 启动线 程 AfxBeginThread(ThreadProc12, NULL); AfxBeginThread(ThreadProc13, NULL); // 等待计算完毕 Sleep(300); // 报告计算结果 CString sResult = CString(g_cArray); AfxMessageBox(sResult); } HANDLE OpenEvent( DWORD dwDesiredAccess, // 访问标 志 BOOL bInheritHandle, // 继承标志 LPCTSTR lpName // 指向事件对象名的指针 ); 如果事件对象已创建(在创建事件时需要指定事件名),函数将返回指定事件的 句柄。对于那些在创建事件时没有指定事件名的事件内核对象,可以通过使用内核 对象的继承性或是调用DuplicateHandle()函数来调用CreateEvent()以获得对指定 事件对象的访问权 。在获取到访问权 后所进行的同步操作与在同一个进程中所进行 的线程同步操作是一样的。 如果需要在一个线程中等待多个事件,则用 WaitForMultipleObjects()来等待。 WaitForMultipleObjects()与 WaitForSingleObject ()类似,同时监视 位于句柄数组中 的所有句柄。这些被监视对 象的句柄享有平等的优先权,任何一个句柄都不可能比 其他句柄具有更高的优先权。 WaitForMultipleObjects()的函数原型为: 参数 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 ()函数的使用。通过对 两个事件内核对象的等待来控制线程任务的执行与中途退出 : DWORD WaitForMultipleObjects( DWORD nCount, // 等待句柄数 CONSTHANDLE*lpHandles, // 句柄数组首地址 BOOL fWaitAll, // 等待标志 DWORD dwMilliseconds // 等待时间间 隔 ); // 存放事件句柄的数组 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); // 设置事件 0(开启事件) SetEvent(hEvents[0]); } void CSample08View::OnEndevent() { // 设置事件 1(结束事件) SetEvent(hEvents[1]); MFC 为事件相关处 理也提供了一个 CEvent 类,共包含有除构造函数外的 4个成 员函数 PulseEvent()、ResetEvent()、 SetEvent()和 UnLock()。在功能上分别相当 与Win32 API 的PulseEvent()、ResetEvent()、SetEvent()和 CloseHandle()等函数。 而构造函数则履行了原 CreateEvent()函数创建事件对象的职责 ,其函数原型为: 按照此缺省设置将创建一个自动复 位、初始状态为复 位状态的没有名字的事件 对象。封装后的 CEvent 类使用起来更加方便,图2即展示了 CEvent 类对 A、B两线 程的同步过 程: 图2 CEvent 类对线 程的同步过 程示意 B线程在执行到 CEvent 类成员函数 Lock()时将会发生阻塞,而 A线程此时则 可 以在没有 B线程干扰的情况下对共享资源进行处理,并在处理完成后通过成员函数 SetEvent()向 B发出事件,使其被释放,得以对A先前已处理完毕的共享资源进行 操作。可见,使用 CEvent 类对线 程的同步方法与通过API 函数进行线程同步的处 理方法是基本一致的。前面的 API 处理代码可用 CEvent 类将其改写为: } CEvent(BOOL bInitiallyOwn = FALSE,BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL); // MFC 事件类对 象 CEvent g_clsEvent; UINT ThreadProc22(LPVOID pParam) { // 对共享资源进行写入操作 for (int i = 0; i < 10; i++) { 信号量内核对象 信号量(Semaphore)内核对象对线 程的同步方式与前面几种方法不同,它允许多 个线程在同一时刻访问 同一资源,但是需要限制在同一时刻访问 此资源的最大线程 数目。在用 CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和 g_cArray[i] = ’a’; Sleep(1); } // 事件置位 g_clsEvent.SetEvent(); return 0; } UINT ThreadProc23(LPVOID pParam) { // 等待事件 g_clsEvent.Lock(); // 对共享资源进行写入操作 for (int i = 0; i < 10; i++) { g_cArray[10 - i - 1] = ’b’; Sleep(1); } return 0; } …… void CSample08View::OnEventMfc() { // 启动线 程 AfxBeginThread(ThreadProc22, NULL); AfxBeginThread(ThreadProc23, NULL); // 等待计算完毕 Sleep(300); // 报告计算结果 CString sResult = CString(g_cArray); AfxMessageBox(sResult); } 当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个 线程对共享资源的访问 ,当前可用资源计数就会减 1,只要当前可用资源计数是大 于0的,就可以发出信号量信号。但是当前可用计数减小到 0时则说 明当前占用资 源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信 号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过 ReleaseSemaphore()函数将当前可用资源计数加 1。在任何时候当前可用资源计数 决不可能大于最大资源计数。 图3 使用信号量对象控制资源 下面结合图例3来演示信号量对象对资 源的控制。在图3中,以箭头和白色箭头 表示共享资源所允许的最大资源计数和当前可用资源计数。初始如图(a)所示,最大 资源计数和当前可用资源计数均为4,此后每增加一个对资 源进行访问 的线程(用黑 色箭头表示)当前资源计数就会相应减1,图(b)即表示的在 3个线程对共享资源进 行访问时 的状态。当进入线程数达到 4个时,将如图(c)所示,此时已达到最大资源 计数,而当前可用资源计数也已减到 0,其他线程无法对共享资源进行访问 。在当前 占有资源的线程处理完毕而退出后,将会释放出空间,图(d)已有两个线程退出对资 源的占有,当前可用计数为2,可以再允许2个线程进入到对资 源的处理。可以看出 , 信号量是通过计 数来对线 程访问资 源进行控制的,而实际 上信号量确实也被称作 Dijkstra 计数器。 使用信号量内核对象进行线程同步主要会用到 CreateSemaphore()、 OpenSemaphore()、ReleaseSemaphore()、 WaitForSingleObject()和 WaitForMultipleObjects()等函数。其中,CreateSemaphore()用来创建一个信号量内 核对象,其函数原型为: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针 LONG lInitialCount, // 初始计数 LONG lMaximumCount, // 最大计数 LPCTSTR lpName // 对象名指针 ); 参数 lMaximumCount 是一个有符号 32 位值,定义了允许的最大资源计数,最大 取值不能超过4294967295。lpName 参数可以为创 建的信号量定义一个名字,由于其 创建的是一个内核对象,因此在其他进程中可以通过该 名字而得到此信号量。 OpenSemaphore()函数即可用来根据信号量名打开在其他进程中创建的信号量,函 数原型如下: 在线程离开对 共享资源的处理时,必须通过ReleaseSemaphore()来增加当前可 用资源计数。否则将会出现当前正在处理共享资源的实际线 程数并没有达到要限制 的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。 ReleaseSemaphore()的函数原型为: 该函数将 lReleaseCount 中的值添加给信号量的当前资源计数,一般将 lReleaseCount 设置为1,如果需要也可以设置其他的值。 WaitForSingleObject()和 WaitForMultipleObjects()主要用在试图进 入共享资源的线程函数入口处,主要用来 判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数 值大于 0时,被监视 的信号量内核对象才会得到通知。 信号量的使用特点使其更适用于对Socket(套接字)程序中线程的同步。例如, 网络上的 HTTP 服务器要对同一时间 内访问 同一页面的用户数加以限制,这时 可以 为没一个用户对 服务器的页面请求设置一个线程,而页面则是待保护的共享资源 , 通过使用信号量对线 程的同步作用可以确保在任一时刻无论有多少用户对 某一页 面进行访问 ,只有不大于设定的最大用户数目的线程能够进 行访问 ,而其他的访问 HANDLE OpenSemaphore( DWORD dwDesiredAccess, // 访问标 志 BOOL bInheritHandle, // 继承标志 LPCTSTR lpName // 信号量名 ); BOOL ReleaseSemaphore( HANDLE hSemaphore, // 信号量句柄 LONG lReleaseCount, // 计数递增数量 LPLONG lpPreviousCount // 先前计数 ); 企图则 被挂起,只有在有用户退出对此页面的访问 后才有可能进入。下面给出的示 例代码即展示了类似的处理过程: // 信号量对象句柄 HANDLE hSemaphore; UINT ThreadProc15(LPVOID pParam) { // 试图进 入信号量关口 WaitForSingleObject(hSemaphore, INFINITE); // 线程任务处 理 AfxMessageBox("线程一正在执行!"); // 释放信号量计数 ReleaseSemaphore(hSemaphore, 1, NULL); return 0; } UINT ThreadProc16(LPVOID pParam) { // 试图进 入信号量关口 WaitForSingleObject(hSemaphore, INFINITE); // 线程任务处 理 AfxMessageBox("线程二正在执行!"); // 释放信号量计数 ReleaseSemaphore(hSemaphore, 1, NULL); return 0; } UINT ThreadProc17(LPVOID pParam) { // 试图进 入信号量关口 WaitForSingleObject(hSemaphore, INFINITE); // 线程任务处 理 AfxMessageBox("线程三正在执行!"); // 释放信号量计数 ReleaseSemaphore(hSemaphore, 1, NULL); return 0; } …… 图4 开始进入的两个线程 图5 线程二退出后线程三才得以进入 上述代码在开启线程前首先创建了一个初始计数和最大资源计数均为2的信号 量对象hSemaphore。即在同一时刻只允许2个线程进入由 hSemaphore 保护的共 享资源。随后开启的三个线程均试图访问 此共享资源,在前两个线程试图访问 共享 资源时,由于 hSemaphore 的当前可用资源计数分别为 2和1,此时的hSemaphore 是可以得到通知的,也就是说位于线程入口处的WaitForSingleObject()将立即返回, 而在前两个线程进入到保护区域后,hSemaphore 的当前资源计数减少到 0, hSemaphore 将不再得到通知, WaitForSingleObject()将线程挂起。直到此前进入到 保护区的线程退出后才能得以进入。图4和图5为上述代脉的运行结果。从实验结 void CSample08View::OnSemaphore() { // 创建信号量对象 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL); // 开启线程 AfxBeginThread(ThreadProc15, NULL); AfxBeginThread(ThreadProc16, NULL); AfxBeginThread(ThreadProc17, NULL); } 果可以看出,信号量始终保持了同一时刻不超过2个线程的进入。 在MFC 中,通过CSemaphore 类对 信号量作了表述。该类 只具有一个构造函数, 可以构造一个信号量对象,并对初始资源计数、最大资源计数、对象名和安全属性 等进行初始化,其原型如下: 在构造了 CSemaphore 类对 象后,任何一个访问 受保护共享资源的线程都必须通 过CSemaphore 从父类CSyncObject 类继 承得到的 Lock()和 UnLock()成员函数来 访问 或释放CSemaphore 对象。与前面介绍的几种通过MFC 类保持线程同步的方 法类似,通过CSemaphore 类也可以将前面的线程同步代码进 行改写,这两种使用 信号量的线程同步方法无论是在实现 原理上还是从实现结 果上都是完全一致的。下 面给出经MFC 改写后的信号量线程同步代码: CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL); // MFC 信号量类对 象 CSemaphore g_clsSemaphore(2, 2); UINT ThreadProc24(LPVOID pParam) { // 试图进 入信号量关口 g_clsSemaphore.Lock(); // 线程任务处 理 AfxMessageBox("线程一正在执行!"); // 释放信号量计数 g_clsSemaphore.Unlock(); return 0; } UINT ThreadProc25(LPVOID pParam) { // 试图进 入信号量关口 g_clsSemaphore.Lock(); // 线程任务处 理 AfxMessageBox("线程二正在执行!"); // 释放信号量计数 g_clsSemaphore.Unlock(); 互斥内核对象 互斥(Mutex)是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资 源的互斥访问 。同临界区有些类似,只有拥有互斥对象的线程才具有访问资 源的权 限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多 个线程所访问 。当前占据资源的线程在任务处 理完后应将拥有的互斥对象交出,以 便其他线程在获得后得以访问资 源。与其他几种内核对象不同,互斥对象在操作系 统中拥有特殊代码,并由操作系统来管理,操作系统甚至还允许其进行一些其他内 核对象所不能进行的非常规操作。为便于理解,可参照图6给出的互斥内核对象的 工作模型: 图6 使用互斥内核对象对共享资源的保护 return 0; } UINT ThreadProc26(LPVOID pParam) { // 试图进 入信号量关口 g_clsSemaphore.Lock(); // 线程任务处 理 AfxMessageBox("线程三正在执行!"); // 释放信号量计数 g_clsSemaphore.Unlock(); return 0; } …… void CSample08View::OnSemaphoreMfc() { // 开启线程 AfxBeginThread(ThreadProc24, NULL); AfxBeginThread(ThreadProc25, NULL); AfxBeginThread(ThreadProc26, NULL); } 图(a)中的箭头为 要访问资 源(矩形框)的线程,但只有第二个线程拥有互斥对象 (黑点)并得以进入到共享资源,而其他线程则会被排斥在外(如图(b)所示)。当此线 程处理完共享资源并准备离开此区域时将把其所拥有的互斥对象交出(如图(c)所 示),其他任何一个试图访问 此资源的线程都有机会得到此互斥对象。 以互斥内核对象来保持线程同步可能用到的函数主要有 CreateMutex()、 OpenMutex()、 ReleaseMutex()、WaitForSingleObject()和 WaitForMultipleObjects() 等。在使用互斥对象前,首先要通过CreateMutex()或 OpenMutex()创建或打开一 个互斥对象。CreateMutex()函数原型为: 参数 bInitialOwner 主要用来控制互斥对象的初始状态。一般多将其设置为 FALSE,以表明互斥对象在创建时并没有为任何线程所占有。如果在创建互斥对象 时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex()函 数得到此互斥对象的句柄。OpenMutex()函数原型为: 当目前对资 源具有访问权 的线程不再需要访问 此资源而要离开时 ,必须通过 ReleaseMutex()函数来释放其拥有的互斥对象,其函数原型为: 其唯一的参数 hMutex 为待释放的互斥对象句柄。至于 WaitForSingleObject()和 WaitForMultipleObjects()等待函数在互斥对象保持线程同步中所起的作用与在其他 内核对象中的作用是基本一致的,也是等待互斥内核对象的通知。但是这里需要特 HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性指针 BOOL bInitialOwner, // 初始拥有者 LPCTSTR lpName // 互斥对象名 ); HANDLE OpenMutex( DWORD dwDesiredAccess, // 访问标 志 BOOL bInheritHandle, // 继承标志 LPCTSTR lpName // 互斥对象名 ); BOOL ReleaseMutex(HANDLE hMutex); 别指出的是:在互斥对象通知引起调用等待函数返回时,等待函数的返回值不再是 通常的 WAIT_OBJECT_0(对于WaitForSingleObject()函数)或是在 WAIT_OBJECT_0 到WAIT_OBJECT_0+nCount-1 之间的一个值(对于WaitForMultipleObjects()函数), 而是将返回一个 WAIT_ABANDONED_0(对于WaitForSingleObject()函数)或是在 WAIT_ABANDONED_0 到WAIT_ABANDONED_0+nCount-1 之间的一个值(对于 WaitForMultipleObjects()函数)。以此来表明线程正在等待的互斥对象由另外一个线 程所拥有,而此线程却在使用完共享资源前就已经终 止。除此之外,使用互斥对象 的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他 内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时失去可 调度性,而使用互斥的方法却可以在等待的同时仍具有可调度性,这也正是互斥对 象所能完成的非常规操作之一。 在编写程序时,互斥对象多用在对那些为多个线程所访问 的内存块的保护上,可 以确保任何线程在处理此内存块时 都对其拥有可靠的独占访问权 。下面给出的示例 代码即通过互斥内核对象hMutex 对共享内存快 g_cArray[]进行线程的独占访问 保 护。下面给出实现 代码清单: // 互斥对象 HANDLE hMutex = NULL; char g_cArray[10]; UINT ThreadProc18(LPVOID pParam) { // 等待互斥对象通知 WaitForSingleObject(hMutex, INFINITE); // 对共享资源进行写入操作 for (int i = 0; i < 10; i++) { g_cArray[i] = ’a’; Sleep(1); } // 释放互斥对象 ReleaseMutex(hMutex); return 0; } UINT ThreadProc19(LPVOID pParam) { 互斥对象在 MFC 中通过CMutex 类进 行表述。使用 CMutex 类的方法非常简单 , 在构造 CMutex 类对 象的同时可以指明待查询 的互斥对象的名字,在构造函数返回 后即可访问 此互斥变量。CMutex 类也是只含有构造函数这唯一的成员函数,当完成 对互斥对象保护资 源的访问 后,可通过调 用从父类CSyncObject 继承的 UnLock( ) 函数完成对互斥对象的释放。CMutex 类构造函数原型为: // 等待互斥对象通知 WaitForSingleObject(hMutex, INFINITE); // 对共享资源进行写入操作 for (int i = 0; i < 10; i++) { g_cArray[10 - i - 1] = ’b’; Sleep(1); } // 释放互斥对象 ReleaseMutex(hMutex); return 0; } …… void CSample08View::OnMutex() { // 创建互斥对象 hMutex = CreateMutex(NULL, FALSE,NULL); // 启动线 程 AfxBeginThread(ThreadProc18, NULL); AfxBeginThread(ThreadProc19, NULL); // 等待计算完毕 Sleep(300); // 报告计算结果 CString sResult = CString(g_cArray); AfxMessageBox(sResult); } CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL); 该类 的适用范围和实现 原理与 API 方式创建的互斥内核对象是完全类似的,但 要简洁 的多,下面给出就是对前面的示例代码经 CMutex 类改写后的程序实现 清单: // MFC 互斥类对 象 CMutex g_clsMutex(FALSE, NULL); UINT ThreadProc27(LPVOID pParam) { // 等待互斥对象通知 g_clsMutex.Lock(); // 对共享资源进行写入操作 for (int i = 0; i < 10; i++) { g_cArray[i] = ’a’; Sleep(1); } // 释放互斥对象 g_clsMutex.Unlock(); return 0; } UINT ThreadProc28(LPVOID pParam) { // 等待互斥对象通知 g_clsMutex.Lock(); // 对共享资源进行写入操作 for (int i = 0; i < 10; i++) { g_cArray[10 - i - 1] = ’b’; Sleep(1); } // 释放互斥对象 g_clsMutex.Unlock(); return 0; } …… void CSample08View::OnMutexMfc() 小结 线程的使用使程序处理更够更加灵活,而这种 灵活同样也会带来各种不确定性 的可能。尤其是在多个线程对同一公共变量进行访问时 。虽然未使用线程同步的程 序代码在逻辑 上或许没有什么问 题 ,但为了确保程序的正确、可靠运行,必须在适 当的场合采取线程同步措施。 { // 启动线 程 AfxBeginThread(ThreadProc27, NULL); AfxBeginThread(ThreadProc28, NULL); // 等待计算完毕 Sleep(300); // 报告计算结果 CString sResult = CString(g_cArray); AfxMessageBox(sResult); }

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ] 1 人已下载

下载文档

相关文档