9.5 信号量内核对象(Semaphore)
(1)信号量的组成
①计数器:该内核对象被使用的次数
②最大资源数量:标识信号量可以控制的最大资源数量(带符号的32位)
③当前资源数量:标识当前可用资源的数量(带符号的32位)。即表示当前开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申请。但这些开放的资源不一定被线程占用完。比如,当前开放5个资源,而只有3个线程申请,则还有2个资源可被申请,但如果这时总共是7个线程要使用信号量,显然开放的资源5个是不够的。这时还可以再开放2个,直到达到最大资源数量。
(2)信号量的使用规则
①如果当前资源计数>0,那么信号量处于触发状态,表示有可用资源。
②如果当前资源计数=0,那么信号量处于未触发状态,表示没有可用资源。
③系统绝不会让当前资源计数变为负数;
④当前资源计数绝对不会大于最大资源计数
(3)信号量的用法
(4)相关函数
①创建信号量CreateSemaphore
参数 |
描述 |
psa |
安全属性 |
lInitialCount |
初始化时,共有多少个资源是可用的。如果该值设为0,表示没有可用资源,此时信号量为未触发状态,任何等待信号数的线程将进入等待状态。以后可通过调用ReleaseSemaphore来增加可用资源,同时变为触发状态。 |
LMaximumCount |
能够处理的最大的资源数量 |
pszName |
信号量的名称 |
②增加信号量:ReleaseSemaphore
参数 |
描述 |
hSemaphore |
信号量句柄 |
lReleaseCount |
将lReleaseCount值加到信号量的当前资源计数上 |
pLPreviousCount |
返回当前资源计数的原始值,一般填NULL |
★调用ReleaseSemaphore可以获得资源计数的原始值,但同时会增加当前资源的计数值。目前还没有办法在不改变当前资源计数值的前提下获得信号量的可用资源数。(即使lReleaseCount填入0也获取不到!)
③Wait*之类的等待函数:当线程调用等待函数时,如果信号量处于触发状态(可用资源大于0),则线程获得信号量的一个资源并把可用资源数量减1,同时继续执行。如果信号量处于未触发状态,则线程进入等待状态。直到其他线程释放资源。
【Semaphore程序】
#include <windows.h> #include <tchar.h> #include <locale.h> ////////////////////////////////////////////////////////////////////////// //一个只能容纳10个客人的餐馆来了12个客户 #define MAX_SEM_COUNT 10 #define THREADCOUNT 12 ////////////////////////////////////////////////////////////////////////// HANDLE g_hSemaphore = NULL; DWORD WINAPI ThreadProc(LPVOID pvParam); ////////////////////////////////////////////////////////////////////////// int _tmain() { _tsetlocale(LC_ALL, _T("chs")); HANDLE aThread[THREADCOUNT] = { NULL }; DWORD dwThreadID = 0; g_hSemaphore = CreateSemaphore(NULL, MAX_SEM_COUNT, MAX_SEM_COUNT, NULL); //以下两句与上面那句的实现的效果一样,先创建无资源的信号量,再增加资源 //g_hSemaphore = CreateSemaphore(NULL, 0, MAX_SEM_COUNT, NULL); //ReleaseSemaphore(g_hSemaphore, MAX_SEM_COUNT, NULL); if (NULL == g_hSemaphore){ _tprintf(_T("创建信号量失败:%d "), GetLastError()); return -1; } //创建12个线程 for (int i = 0; i < THREADCOUNT;i++){ aThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL,0,&dwThreadID); if (aThread[i] == NULL){ _tprintf(_T("创建线程失败:%d "), GetLastError()); return -1; } } //等待所有线程结束 WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE); //关闭所有子线程 for (int i = 0; i < THREADCOUNT;i++){ CloseHandle(aThread[i]); } //关闭信号量 CloseHandle(g_hSemaphore); _tsystem(_T("PAUSE")); return 0; } //线程函数 DWORD WINAPI ThreadProc(LPVOID pvParam) { DWORD dwWaitResult; BOOL bContinue = TRUE; LONG lPrevCount=0; while (bContinue){ //等待资源,立即返回 dwWaitResult = WaitForSingleObject(g_hSemaphore, 0L); switch (dwWaitResult) { case WAIT_OBJECT_0: bContinue = FALSE; _tprintf(_T("线程%d:等待成功! "), GetCurrentThreadId()); Sleep(20); if (!ReleaseSemaphore(g_hSemaphore, 1, &lPrevCount)){ _tprintf(_T("ReleaseSemaphore失败:%d! "), GetLastError()); } //_tprintf(_T("线程%d:等待成功,当前可用资源%d! "), GetCurrentThreadId(),lPrevCount+1); break; case WAIT_TIMEOUT: _tprintf(_T("线程%d:等待超时! "), GetCurrentThreadId()); break; } } _tprintf(_T("线程%d:退出 "), GetCurrentThreadId()); return 0; }
9.6 互斥量内核对象(Mutex)
(1)互斥量的组成
①使用计数:与其他内核对象,都有一个使用计数
②线程ID:标识当前占用这个互斥量是哪个线程
③递归计数:该线程占用互斥量的次数
(2)互斥量的规则
①如果线程ID为0(即无效ID),那么该互斥量不为任何线程所占,即处于触发状态
②如果线程ID为非零值,表示该线程己经占用该互斥量,它处于非触发状态。
③其他内核对象只会记录哪些线程正在等待,但互斥量还会记录哪个线程等待成功。这使得它在未被触发的时候,也可以被线程所获得。当任何线程试图调用ReleasMutex时,该函数会检查调用线程的ID与互斥量内部保存的ID是否一致。如果一致,则递归计数递减。否则返回FALSE,这时调用GetLastError将得到ERROR_NOT_OWNER。
(3)互斥量的使用方法
①创建互斥量:CreateMutext(psa,bInitialOwner,pszName);其中bInitialOwner为TRUE时表示将调用线程ID设为互斥量内部的线程ID,递归计数加1。否则表示互斥量不被占用(处于触发状态)
②释放互斥量:ReleaseMutex,递归计数减1,当递归计数为0时,将线程ID设为0。这里互斥量被触发。
9.6.1 互斥量被“遗弃问题”
如果占用互斥量的线程在释放互斥量之前提前终止,这种现象叫互斥量被“遗弃”(Abandoned),系统会自动将该互斥量的线程ID和递归计数设为0。再“公平地”唤醒正在等待的一个线程,但这时唤醒的线程从Wait*中返回的不再是WAIT_OBJECT_0,而是返回WAIT_ABANDONED,以表示该线程等到了一个被遗弃的互斥量。所以此时被互斥量保护的资源处于什么状态,这个被唤醒的线程并不知道,要求应用程序自己决定怎么做。
9.6.2 互斥量与关键段的比较
特征 |
互斥量 |
关键段 |
性能 |
慢 |
快 |
是否能跨进程使用 |
是 |
否 |
声时 |
HANDLE hmtx |
CRITICAL_SECTION cs; |
初始化 |
CreateMutex |
InitializeCriticalSection |
清理 |
CloseHandle |
DeleteCriticalSection |
无限等待 |
Wait*(hmtx,INFINITE) |
EnterCriticalSection(&cs) |
0等待 |
Wait*(hmtx,0) |
TryEnterCriticalSection |
任意时长等待 |
Wait*(htmx,dwMilliseconds) |
不支持 |
释放 |
ReleaseMutex |
LeaveCriticalSection |
是否能同时等待其他内核对象 |
是 如WaitForMultipleObject |
否 |
【Queue程序】演示生产消费问题(多线程安全队列版)
//Queue.h
#pragma once #include <windows.h> //队列类(线程安全) class CQueue{ public: struct ELEMENT{ int m_nThreadNum; //客户线程号 int m_nRequestNum; //客户请求号 //其他字段应该这之后 }; typedef ELEMENT* PELEMENT; private: PELEMENT m_pElements; //要处理的队列元素的数组,也是需要被保护的数据 int m_nMaxElements; //数组元素最大数量 HANDLE m_h[2]; //互斥量和信号量 HANDLE &m_hmtxQ; //m_h[0]的引用,用来决定是否可以访问队列 HANDLE &m_hsemNumElements; //m_h[1]的引用,用来判断队列是否有可用的空间 public: CQueue(int nMaxElements); ~CQueue(); BOOL Append(PELEMENT pElement, DWORD dwMilliseconds);//增加一个元素 BOOL Remove(PELEMENT pElement, DWORD dwMilliseconds); //从队列取出一个元素 };
//Queue.cpp
#include "Queue.h" ////////////////////////////////////////////////////////////////////////// CQueue::CQueue(int nMaxElements) :m_hmtxQ(m_h[0]), m_hsemNumElements(m_h[1]) { m_pElements = (PELEMENT)HeapAlloc(GetProcessHeap(), 0, sizeof(ELEMENT)*nMaxElements); m_nMaxElements = nMaxElements; //创建互斥量和信号量 m_hmtxQ = CreateMutex(NULL, FALSE, NULL); m_hsemNumElements = CreateSemaphore(NULL, 0, nMaxElements, NULL); } ////////////////////////////////////////////////////////////////////////// CQueue::~CQueue() { CloseHandle(m_hsemNumElements); CloseHandle(m_hmtxQ); HeapFree(GetProcessHeap(), 0, m_pElements); } ////////////////////////////////////////////////////////////////////////// BOOL CQueue::Append(PELEMENT pElement, DWORD dwMillseconds) { BOOL fOk = FALSE; //以独占方式访问队列 DWORD dw = WaitForSingleObject(m_hmtxQ, dwMillseconds); if (dw == WAIT_OBJECT_0){ //判断队列是否己满(即信号量中是否还有可用资源) //因为增加可用资源的数量(每次增加1),直到被用完(m_nMaxElements) //所以lPrevCount返回上次可用资源的数量 LONG lPrevCount; fOk = ReleaseSemaphore(m_hsemNumElements, 1, &lPrevCount); if (fOk){ //增加1个资源,如果成功说明资源还没用完,仍有可用资源 //队列未满,则增加一个新元素 m_pElements[lPrevCount] = *pElement; } else{ //队列己满,设置出错代码,并返回FALSE SetLastError(ERROR_DATABASE_FULL); } //释放互斥锁,允许其他线程访问队列 ReleaseMutex(m_hmtxQ); } else{ //超时,设置出错代码并返回FALSE SetLastError(ERROR_TIMEOUT); } return fOk; } ////////////////////////////////////////////////////////////////////////// //读取(并删除)队列中的元素 BOOL CQueue::Remove(PELEMENT pElement, DWORD dwMillseconds) { //要读取,首先要确保对队的独占访问权,所以要等待互斥锁。 //其次,队列要有元素,即信号量要有可用资源(即信号量是触发状态) //只有当这两个条件都满足时,才会唤醒服务线程来读取元素。 BOOL fOk =(WAIT_OBJECT_0 == WaitForMultipleObjects(_countof(m_h),m_h,TRUE,dwMillseconds)); if (fOk){ //队列至少有一个元素,从队列中读取出来 *pElement = m_pElements[0]; //先进先出 //将剩余的元素向前移 MoveMemory(&m_pElements[0], &m_pElements[1], sizeof(ELEMENT)*(m_nMaxElements-1)); //释放互斥锁,以便其他线程访问队列 ReleaseMutex(m_hmtxQ); } else { //超时 SetLastError(ERROR_TIMEOUT); } return fOk; //调用GetLassError来获得更多信息 }
//main.cpp
#include "../../CommonFiles/CmnHdr.h" #include <windows.h> #include "resource.h" #include "Queue.h" #include <tchar.h> #include <strsafe.h> ////////////////////////////////////////////////////////////////////////// CQueue g_q(10); //共享队列 volatile LONG g_bShutdown = FALSE; //标识是否结束客户或服务线程 HWND g_hwnd; //对话框句柄 //客户线程和服务线程 HANDLE g_hThreads[MAXIMUM_WAIT_OBJECTS]; //最多64个线程 int g_nNumThreads; //当前产生的线程总数 ////////////////////////////////////////////////////////////////////////// DWORD WINAPI ClientThread(PVOID pvParam) { int nThreadNum = PtrToUlong(pvParam); HWND hwndLB = GetDlgItem(g_hwnd, IDC_CLIENTS); int nRequestNum = 0; TCHAR sz[1024]; //InterlockedCompareExchange比较参数1与3,如果相等,则 //则g_bShutdown=参数2,否则g_bShutdown不变。返回g_bShubdown //的初始值。即如果g_bShutdown=false,则返回0,如果g_bShutdown=true //则返回true while (1 != InterlockedCompareExchange(&g_bShutdown,0,0)){ //追踪当前线程的请求号 nRequestNum++; //每个线程的请求号,不断递增 CQueue::ELEMENT e = { nThreadNum, nRequestNum }; //尝试将元素放入队列中 if (g_q.Append(&e,200)){ // StringCchPrintf(sz, _countof(sz), TEXT("线程%d,发送请求%d"), nThreadNum,nRequestNum); } else { //不能将元素放入队列中 StringCchPrintf(sz, _countof(sz), TEXT("线程%d,发送请求%d失败(%s)"), nThreadNum,nRequestNum,(GetLastError()==ERROR_TIMEOUT)? TEXT("超时"):TEXT("队列己满")); } //显示增加结果 ListBox_SetCurSel(hwndLB, ListBox_AddString(hwndLB,sz)); Sleep(2500); //在增加另一新元素时休眠2.5秒 } return 0; } ////////////////////////////////////////////////////////////////////////// DWORD WINAPI ServerThread(PVOID pvParam) { int nThreadNum = PtrToUlong(pvParam); HWND hwndLB = GetDlgItem(g_hwnd, IDC_SERVERS); TCHAR sz[1024]; while (1 != InterlockedCompareExchange(&g_bShutdown, 0, 0)){ CQueue::ELEMENT e; //尝试从队列中获取一个元素 if (g_q.Remove(&e,5000)){ //标明是哪个服务线程处理队列中的元素 StringCchPrintf(sz, _countof(sz), TEXT("服务线程%d处理了客户%d的%d号请求!"), nThreadNum, e.m_nThreadNum,e.m_nRequestNum); //让服务线程消费速度慢一点(否则可能会超过客户生产数据的速度) Sleep(2000*e.m_nThreadNum); } else{ //无法从队列中获取得元素 StringCchPrintf(sz, _countof(sz), TEXT("服务线程%d:(超时)"),nThreadNum); } //显示处理结果 ListBox_SetCurSel(hwndLB,ListBox_AddString(hwndLB,sz)); } return 0; } ////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_QUEUE); g_hwnd = hwnd; DWORD dwThreadID; //创建客户线程 for (int x = 0; x < 4;x++) g_hThreads[g_nNumThreads++] = chBEGINTHREADEX(NULL, 0, ClientThread, (PVOID)(INT_PTR)x, 0, &dwThreadID); //创建服务线程 for (int x = 0; x < 2;x++){ g_hThreads[g_nNumThreads++] = chBEGINTHREADEX(NULL, 0, ServerThread, (PVOID)(INT_PTR)x, 0, &dwThreadID); } return TRUE; } ////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtrl, UINT codeNotity) { switch (id) { case IDCANCEL: EndDialog(hwnd, id); break; } } ////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); } return FALSE; } ////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) { DialogBox(hInstExe, MAKEINTRESOURCE(IDD_QUEUE), NULL, Dlg_Proc); //退出客户和服务线程 InterlockedExchange(&g_bShutdown, TRUE); //等待所有线程结束 WaitForMultipleObjects(g_nNumThreads, g_hThreads, TRUE, INFINITE); while (g_nNumThreads--) CloseHandle(g_hThreads[g_nNumThreads]); return 0; }
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 9_Queue.rc 使用 // #define IDD_QUEUE 101 #define IDI_QUEUE 102 #define IDC_SERVERS 1001 #define IDC_CLIENTS 1002 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 103 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1004 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//Queue.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h " END 2 TEXTINCLUDE BEGIN "#include ""winres.h"" " "