信 号量(Semaphore )内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最 大线程数目。在用CreateSemaphore () 创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最 大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1 ,只要当前可用资源计数是大于0 的,就可以发出信号量信号。但是当前可用计数减小 到0 时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离 开的同时通过ReleaseSemaphore ()函数将当前可用资源计数加1 。在任何时候当前可用资源计数决不可能大于最大资源计数。 信号量是通过计数来对线程访问资源进行控制的,而实际上信号量确实也被称作Dijkstra 计数器。 使用信号量内核对象进行线程同步主要会用到CreateSemaphore ()、OpenSemaphore ()、 ReleaseSemaphore ()、WaitForSingleObject ()和WaitForMultipleObjects ()等函数。其 中,CreateSemaphore ()用来创建一个信号量内核对象,其函数原型为: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针 LONG lInitialCount, // 初始计数 LONG lMaximumCount, // 最大计数 LPCTSTR lpName // 对象名指针 ); 参数lMaximumCount 是一个有符号32 位值,定义了允许的最大资源计数,最大取值不能超过4294967295 。lpName 参数可以为创建的 信号量定义一个名字,由于其创建的是一个内核对象,因此在其他进程中可以通过该名字而得到此信号量。OpenSemaphore ()函数即可用来根据信号 量名打开在其他进程中创建的信号量,函数原型如下: HANDLE OpenSemaphore( DWORD dwDesiredAccess, // 访问标志 BOOL bInheritHandle, // 继承标志 LPCTSTR lpName // 信号量名 ); 在线程离开对共享资源的处理时,必须通过ReleaseSemaphore ()来增加当前可用资源计数。否则将会出现当前正在处理共享资源的实际线程数并 没有达到要限制的数值,而其他线程却因为当前可用资源计数为0 而仍无法进入的情况。ReleaseSemaphore ()的函数原型为: BOOL ReleaseSemaphore( HANDLE hSemaphore, // 信号量句柄 LONG lReleaseCount, // 计数递增数量 LPLONG lpPreviousCount // 先前计数 ); 该函数将lReleaseCount 中的值添加给信号量的当前资源计数,一般将lReleaseCount 设置为1 ,如果需要也可以设置其他的值。 WaitForSingleObject ()和WaitForMultipleObjects ()主要用在试图进入共享资源的线程函数入口处,主要用来判 断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0 时,被监视的信号量内核对象才会得到通知。 信号量的使用特点使其更适用于对Socket (套接字)程序中线程的同步。例如,网络上的HTTP 服务器要对同一时间内访问同一页面的用户数加以限制,这 时可以为每一个用户对服务器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某 一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。下面给出 的示例代码即展示了类似的处理过程: // 信号量对象句柄 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; } …… void CSample08View::OnSemaphore() { // 创建信号量对象 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL); // 开启线程 AfxBeginThread(ThreadProc15, NULL); AfxBeginThread(ThreadProc16, NULL); AfxBeginThread(ThreadProc17, NULL); } 上述代码在开启线程前首先创建了一个初始计数和最大资源计数均为2 的信号量对象hSemaphore 。即在同一时刻只允许2 个线程进入由 hSemaphore 保护的共享资源。随后开启的三个线程均试图访问此共享资源,在前两个线程试图访问共享资源时,由于hSemaphore 的当前可用资 源计数分别为2 和1 ,此时的hSemaphore 是可以得到通知的,也就是说位于线程入口处的WaitForSingleObject ()将立即返回,而 在前两个线程进入到保护区域后,hSemaphore 的当前资源计数减少到0 ,hSemaphore 将不再得到通 知,WaitForSingleObject ()将线程挂起。直到此前进入到保护区的线程退出后才能得以进入。图4 和图5 为上述代脉的运行结果。从实验结 果可以看出,信号量始终保持了同一时刻不超过2 个线程的进入。 在MFC 中,通过CSemaphore 类对信号量作了表述。该类只具有一个构造函数,可以构造一个信号量对象,并对初始资源计数、最大资源计数、对象名和安全属性等进行初始化,其原型如下: CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL ); 在构造了CSemaphore 类对象后,任何一个访问受保护共享资源的线程都必须通过CSemaphore 从父类CSyncObject 类继承得到的 Lock ()和UnLock ()成员函数来访问或释放CSemaphore 对象。与前面介绍的几种通过MFC 类保持线程同步的方法类似,通过 CSemaphore 类也可以将前面的线程同步代码进行改写,这两种使用信号量的线程同步方法无论是在实现原理上还是从实现结果上都是完全一致的。下面给 出经MFC 改写后的信号量线程同步代码: // 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(); 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); } |