第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)
8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的!
8.4.1 关键段的细节
(1)CRITICAL_SECTION的使用方法
①CRITICAL_SECTION cs; //声明为全局变量(也可是成员变量,甚至局部变量)
②InitializeCriticalSection(&cs); //初始化临界区,注意cs是临界区对象,不能被移动和复制
③EnterCriticalSection(&cs); //进入或等待临界区(任何要访问共享资源的代码都须包含应在
//Enter和Leave之间,如果忘了哪怕一个地方,都可能破坏资源)
④LeaveCriticalSection(&cs); //离开临界区
⑤DeleteCriticalSection(&cs); //不再需要临界区对象。
(2)CRITICAL_SECTION数据结构
①LockCount字段:最重要的字段,初始化为-1。该字段在XP和Vista以后版本含义有所不同,在Vista以后版本中。
A、最低位——0表示临界区被锁,1表示没被锁。-->0x1 & LockCount
B、第2位(低位数起):1表示没有线程被唤醒。0表示一个线程被唤醒 -->(0x2 & lockCount)>>1;
C、其余各位表示等待锁的线程数量(-1-lockCount)>>2
②RecursionCount:表示拥有者线程己经获得该临界区的次数,初始值为0。当拥有者线程每调用EnterCriticalSection时会递增1。也就是说只有拥有者调用EnterCriticalSection时RecursionCount才递增。但为了防止拥有者线程一直霸占临界区,系统允许其他线程调用LeaveCriticalSection使该值递减。但不管是拥有者线程还是其他线程,调用Leave的总次数应该等于Enter的次数,才能被解开锁,当RecursionCount<=0时,锁就解开,OwningThread被设为0。
③OwningThread:此字段表示当前占用该临界区的线程ID
④LockSemaphore:此字段命名不恰当,它实际上是一个事件对象,用于通知操作系统该临界区已被释放,等待该临界区的线程之一现在可以获得该临界区并继续执行。因为系统是在临界区阻止另一个线程时才自动分配事件句柄,所以如果在不再需要临界区时要将其删除,以防止LockSemaphore 字段可能会导致内存泄漏。
(3)EnterCriticalSection函数的执行过程
①如果没有线程正在访问资源,那么EnterCritical会更新临界区成员变量,以表示调用线程己经获得临界区锁,并立即返回,如此调用线程继续执行。
②当调用线程获得临界区锁后,如果此时调用线程再次Enter,则会更新RecursionCount,以表示调用线程被获准访问的次数。并立即返回。
③当调用线程获得临界区锁后,如果此时是其他线程要进入临界区,则EnterCriticalSection会使用一个事件内核对象(lockSemaphore)将这个线程切换到等待状态。(注意,这些等待的线程会事件内核对象记录下来,以表示正在等待该内核对象的都有哪些线程,当然线程本身也会记录,他在等哪些内核对象)(注意:临界区本身不是内核对象!)
(4)LeaveCriticalSection函数:会更新CRITICAL_SECTION的成员变量,如果此时仍有线程处于等待状态,那么该函数会将其中之一的等待线程换回可调度状态。
(5)TryEnterCriticalSection函数
①该函数不会让调用线程进入等待状态,它通过返回值来表示是否获准访问资源。TRUE表示获准,FALSE表示其他线程正在使用资源,申请被拒绝。
②如果返回TRUE时,说明该调用线程己经正在访问资源,CRITICAL_SECTION 成员变量被更新过。所以每个返回TRUE的TryEnterCriticalSection都须调用LeaveCriticalSection。
【CriticalsectionInfo程序】
#include <windows.h> #include <malloc.h> #include <tchar.h> #include <locale.h> #define THREADNUM 3 CRITICAL_SECTION g_cs; HANDLE* hThread = NULL; /* 临界区中LockCount和RecursionCount字段的含义 1、XP和Windows2000下 (1)LockCount: ①初始为-1,每调用EnterCriticalSection时LockCount加1,调用LeaveCriticalSection减1 ②如LockCount = 5 表示某一线程正在使用临界区,此外还有5个线程正在等待锁 (2)RecursionCount:调用线程多次调用EnterCriticalSection的次数 (3)EntryCount:除了调用线程以外的其他线程调用EnterCriticalSection的次数。 (4)当第1次调用EnterCriticalSection后,LockCount、RecursionCount、EntryCount、ContentionCount各加1, OwningThread设为调用线程的ID。 A、当拥有者再次调用EnterCriticalSection:LockCount++,Recursion++、EntryCount不变 B、当其他线程调用EnterCriticalSection:LockCount++、EntryCount++、Recursion不变 C、当拥有者调用LeaveCriticalSection:LockCount减1(到-1)、Recursion减到0,OwningThread设为0. D、其他线程调用LeaveCriticalSection,与拥有者调用LeaveCriticalSection变化一样。 2、Windows2003sp1及以后 (1)LockCount: A、最低位——0表示临界区被锁,1表示没被锁。-->0x1 & LockCount B、第2位(低位数起):1表示没有线程被唤醒。0表示一个线程被唤醒 -->(0x2 & lockCount)>>1; C、其余各位表示等待锁的线程数量(-1-lockCount)>>2 */ ////////////////////////////////////////////////////////////////////////// void ShowCriticalSectionInfo(PCRITICAL_SECTION pcs) { _tprintf(_T("Thread[%d],CriticalSection Information: "),GetCurrentThreadId()); _tprintf(_T("--------------------------- ")); _tprintf(_T("LockCount:%d(临界区被锁:%s;是否有线程唤醒:%s;等待锁的线程数量:%d) "), pcs->LockCount, (0x1 & pcs->LockCount) ? _T("否") : _T("是"), ((0x2 & pcs->LockCount)) >> 1 ? _T("否") : _T("是"), ((-1 - pcs->LockCount) >> 2)); _tprintf(_T("RecursionCount:%d "), pcs->RecursionCount); _tprintf(_T("OwningThread:%d "), pcs->OwningThread); _tprintf(_T("LockSemephore:0x%08X "), pcs->LockSemaphore); //_tprintf(_T("SpinCount:0x%08X "), pcs->SpinCount); } ////////////////////////////////////////////////////////////////////////// //线程函数1 DWORD WINAPI ThreadProc1(PVOID pParam) { //1、演示First进程两次进入临界区。 _tprintf(_T("第1个子线程[%d]两次进入临界区 "),GetCurrentThreadId()); EnterCriticalSection(&g_cs); EnterCriticalSection(&g_cs); ShowCriticalSectionInfo(&g_cs); return 0; } //线程函数2 DWORD WINAPI ThreadProc2(PVOID pParam) { int nIndex = (int)pParam; //第nIndex个子程线进入临界区,并拿到锁 _tprintf(_T(" 第%d个子线程[%d]尝试进入临界区 "), nIndex,GetCurrentThreadId()); EnterCriticalSection(&g_cs); ShowCriticalSectionInfo(&g_cs); _tprintf(_T(" 第%d个子线程己经进入临界区...... "), nIndex, GetCurrentThreadId()); _tprintf(_T(" 第%d个子线程线程[%d]离开临界区 "), nIndex, GetCurrentThreadId()); LeaveCriticalSection(&g_cs); ShowCriticalSectionInfo(&g_cs); return 0; } int _tmain() { _tsetlocale(LC_ALL, _T("chs")); InitializeCriticalSection(&g_cs);//初始化临界区时的状态: hThread = (HANDLE*)malloc(sizeof(HANDLE)*THREADNUM); hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, CREATE_SUSPENDED, NULL); hThread[1] = CreateThread(NULL, 0, ThreadProc2, (LPVOID)2, CREATE_SUSPENDED, NULL); hThread[2] = CreateThread(NULL, 0, ThreadProc2, (LPVOID)3, CREATE_SUSPENDED, NULL); ResumeThread(hThread[0]); WaitForSingleObject(hThread[0], INFINITE); _tsystem(_T("PAUSE")); //演示在子线程拥有临界区,但在其他线程(主线程)释放(因子线程两次Enter,主线程要两次Leave) _tprintf(_T(" 主线程[%d]解开临界区锁 "), GetCurrentThreadId()); LeaveCriticalSection(&g_cs); LeaveCriticalSection(&g_cs); ShowCriticalSectionInfo(&g_cs); _tsystem(_T("PAUSE")); //第2个子线程启动,进入尝试进入临界区(应该可行,因为锁被主线程释放) ResumeThread(hThread[1]); WaitForSingleObject(hThread[1], INFINITE); _tsystem(_T("PAUSE")); //主线程锁一下临界区,并启动第3个线程 _tprintf(_T(" 主线程[%d]锁定临界区 "), GetCurrentThreadId()); EnterCriticalSection(&g_cs); ResumeThread(hThread[2]); //此时第3个线程进入等待状态 Sleep(1000); ShowCriticalSectionInfo(&g_cs); //主线程解开临界区锁,并恢复第3个线程 _tprintf(_T(" 主线程[%d]解开临界区锁 "), GetCurrentThreadId()); LeaveCriticalSection(&g_cs); Sleep(1000); _tsystem(_T("PAUSE")); WaitForMultipleObjects(THREADNUM, hThread, TRUE, INFINITE); for (int i = 0; i < THREADNUM;i++){ CloseHandle(hThread[i]); } free(hThread); DeleteCriticalSection(&g_cs); return 0; }
8.4.2 关键段和旋转锁
(1)当一个线程试图进入关键段,但这个关键段正被另一个线程占用时,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式,CPU开销比较大。
(2)但往往当前占用资源的线程可能很快就结束对资源的访问,事实上,在需要等待的线程完成切换到内核模式之前,占用资源的线程可以己经释放了资源,这无法浪费大量CPU时间。
(3)为了提高关键段的性能,可加入合并旋转锁到关键段中。当调用EnterCriticalSection时,先尝试旋转方式的访问资源。只有尝试一个后仍失败。才切换到内核模式并进入等待状态。
(4)要使用具有旋转方式的关键段,必须调用以下函数来初始化关键段 InitializeCriticalSectionAndSpinCount(pcs,dwSpinCount);其中 dwSpinCount为旋转次数(如4000)。如果在单CPU机器上,系统会忽图dwSpinCount参数。
(5)改变关键段的旋转次数SetCriticalSectionSpinCount(pcs,dwSpinCount);
8.4.3 关键段和错误处理
(1)InitializeCriticalSection返回值为VOID,这是Microsoft设计时考虑不周。实际上该函数调用仍可能失败,如给关键分配内存时,当失败时将抛出STATUS_NO_MEMORY异常。
(2)InitializeCriticalSectionAndSpinCount失败时将返回FALSE
(3)关键段内部使用的事件内核对象只有在两个(或多个)线程在同一时刻争夺同一关键段时才会创建它。这样做是为了节省系统资源。只有在DeleteCriticalSection后,该内核对象才会被释放(因此,用完关键段后,不要忘了调用DeleteCriticalSection函数)
(4)在EnterCriticalSection函数中,仍有发生潜在的异常,如创建事件内核对象时,可能会抛出EXCEPTION_INVALID_HANDLE异常。要解决这个问题有两种方法,其一是用结构化异常处理来捕获错误。还有一种是选择InitializeCriticalSectionAndSpinCount来创建关键段,传将dwSpinCount最高位设为1,即告诉系统初始化关键段时就创建一个相关联的事件内核对象,如果无法创建该函数返回FALSE。
(5)注意死锁
用临界区资源使多线程同步时候要特别注意线程死锁问题,假设程序有两临界资源(g_csA、g_csB)与两个子线程(子线程 A、子线程 B),子线程执行体流程如下图(图1)表示,当子线程 A 先获得临界资源 g_csA 后由于子线程 A 的时间片用完了,所以跳到子线程 B 进行执行,这时 B 将获得临界资源 g_csB,然后由于 A 获得临界资源 g_csA,所以 B 只好等待直至子线程B时间片用完,然后跳到子线程 A 继续执行,但是这时的临界资源 g_csB 已经被子线程 B占有,所以子线程 A 有进行等待直至时间片用完。于是子线程A与子线程B就进入了死锁现象流程如下图所示(图2)。
【CriticalSection程序】——模拟多线程对同一内存进行的各种操作(分配、释放、读、写)
#include <windows.h> #include <tchar.h> #include <locale.h> #include <time.h> ////////////////////////////////////////////////////////////////////////// CRITICAL_SECTION g_cs = { 0 }; DWORD* g_pdwAnyData = NULL; //模拟对这个全局的数据指针进行分配、写入、释放等操作 void AllocData(); void WriteData(); void ReadData(); void FreeData(); ////////////////////////////////////////////////////////////////////////// DWORD WINAPI ThreadProc(PVOID pvParam); #define THREADCOUNT 20 #define THREADACTCNT 20 //每个线程执行10次动作 int _tmain() { _tsetlocale(LC_ALL, _T("chs")); srand((unsigned int)time(NULL)); //初始化旋转式的临界区(旋转次数1024次,同时临界区内部创建事件内核对象) if (!InitializeCriticalSectionAndSpinCount(&g_cs, 0x80000400)) return 0; HANDLE aThread[THREADCOUNT]; DWORD dwThreadID = 0; SYSTEM_INFO si = { 0 }; GetSystemInfo(&si); for (int i = 0; i < THREADCOUNT;i++){ aThread[i] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL,CREATE_SUSPENDED,&dwThreadID); //设置线程的亲缘性 SetThreadAffinityMask(aThread[i], 1 << (i % si.dwNumberOfProcessors)); ResumeThread(aThread[i]); } WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE); for (int i = 0; i < THREADCOUNT;i++){ CloseHandle(aThread[i]); } DeleteCriticalSection(&g_cs); //不要忘了删除临界区对象 if (NULL != g_pdwAnyData){ free(g_pdwAnyData); g_pdwAnyData = NULL; } return 0; } //线程函数 DWORD WINAPI ThreadProc(PVOID pvParam) { //为了增加冲突的可能性,让每个线程执行时, //会随机分配20项任务(如分配内存、读取、释放等) int iAct = rand() % 4; int iActCnt = THREADACTCNT; while (iActCnt--){ switch (iAct) { case 0:AllocData();break; case 1:FreeData(); break; case 2:WriteData(); break; case 3:ReadData(); break; } iAct = rand() % 4; } return 0; } void AllocData() { EnterCriticalSection(&g_cs); __try{ _tprintf(_T("线程[ID:0x%X] Alloc Data "), GetCurrentThreadId()); if (NULL == g_pdwAnyData){ g_pdwAnyData = (DWORD*)malloc(sizeof(DWORD)); _tprintf(_T("g_pdwAnyData为NULL,现己分配内存(Address:0x%08X) "), g_pdwAnyData); } else{ _tprintf(_T("g_pdwAnyData不为NULL,无法分配内存(Address:0x%08X) "), g_pdwAnyData); } } __finally{ LeaveCriticalSection(&g_cs); } } void WriteData() { EnterCriticalSection(&g_cs); __try{ _tprintf(_T("线程[ID:0x%X] Write Data "), GetCurrentThreadId()); if (NULL != g_pdwAnyData){ *g_pdwAnyData = rand(); _tprintf(_T("g_pdwAnyData不为空,写入数据(%u) "), *g_pdwAnyData); } else{ _tprintf(_T("g_pdwAnyData为NULL,不能写入数据! ")); } } __finally{ LeaveCriticalSection(&g_cs); } } void ReadData() { EnterCriticalSection(&g_cs); __try{ _tprintf(_T("线程[ID:0x%X] Read Data "), GetCurrentThreadId()); if (NULL != g_pdwAnyData){ _tprintf(_T("g_pdwAnyData不为空,读取数据(%u) "), *g_pdwAnyData); } else{ _tprintf(_T("g_pdwAnyData为NULL,不能读取数据! ")); } } __finally{ LeaveCriticalSection(&g_cs); } } void FreeData() { EnterCriticalSection(&g_cs); __try{ _tprintf(_T("线程[ID:0x%X] Free Data "), GetCurrentThreadId()); if (NULL != g_pdwAnyData){ _tprintf(_T("g_pdwAnyData不为NULL,释放掉! ")); free(g_pdwAnyData); g_pdwAnyData = NULL; } else{ _tprintf(_T("g_pdwAnyData为NULL,不能释放! ")); } } __finally{ LeaveCriticalSection(&g_cs); } }