您的位置:首页 > 其它

第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

2015-08-13 14:06 357 查看
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:\n"),GetCurrentThreadId());
_tprintf(_T("---------------------------\n"));
_tprintf(_T("LockCount:%d(临界区被锁:%s;是否有线程唤醒:%s;等待锁的线程数量:%d)\n"),
pcs->LockCount,
(0x1 & pcs->LockCount) ? _T("否") : _T("是"),
((0x2 & pcs->LockCount)) >> 1 ? _T("否") : _T("是"),
((-1 - pcs->LockCount) >> 2));

_tprintf(_T("RecursionCount:%d\n"), pcs->RecursionCount);
_tprintf(_T("OwningThread:%d\n"), pcs->OwningThread);
_tprintf(_T("LockSemephore:0x%08X\n"), pcs->LockSemaphore);
//_tprintf(_T("SpinCount:0x%08X\n"), pcs->SpinCount);
}

//////////////////////////////////////////////////////////////////////////
//线程函数1
DWORD WINAPI ThreadProc1(PVOID pParam)
{
//1、演示First进程两次进入临界区。
_tprintf(_T("第1个子线程[%d]两次进入临界区\n"),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("\n第%d个子线程[%d]尝试进入临界区\n"), nIndex,GetCurrentThreadId());
EnterCriticalSection(&g_cs);
ShowCriticalSectionInfo(&g_cs);

_tprintf(_T("\n第%d个子线程己经进入临界区......\n"), nIndex, GetCurrentThreadId());

_tprintf(_T("\n第%d个子线程线程[%d]离开临界区\n"), 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("\n主线程[%d]解开临界区锁\n"), 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("\n主线程[%d]锁定临界区\n"), GetCurrentThreadId());
EnterCriticalSection(&g_cs);
ResumeThread(hThread[2]); //此时第3个线程进入等待状态
Sleep(1000);
ShowCriticalSectionInfo(&g_cs);

//主线程解开临界区锁,并恢复第3个线程
_tprintf(_T("\n主线程[%d]解开临界区锁\n"), 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\n\t"), GetCurrentThreadId());
if (NULL == g_pdwAnyData){
g_pdwAnyData = (DWORD*)malloc(sizeof(DWORD));
_tprintf(_T("g_pdwAnyData为NULL,现己分配内存(Address:0x%08X)\n"),
g_pdwAnyData);
} else{
_tprintf(_T("g_pdwAnyData不为NULL,无法分配内存(Address:0x%08X)\n"),
g_pdwAnyData);
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
}

void WriteData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Write Data\n\t"), GetCurrentThreadId());
if (NULL != g_pdwAnyData){
*g_pdwAnyData = rand();
_tprintf(_T("g_pdwAnyData不为空,写入数据(%u)\n"),
*g_pdwAnyData);
} else{
_tprintf(_T("g_pdwAnyData为NULL,不能写入数据!\n"));
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
}

void ReadData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Read Data\n\t"), GetCurrentThreadId());
if (NULL != g_pdwAnyData){
_tprintf(_T("g_pdwAnyData不为空,读取数据(%u)\n"),
*g_pdwAnyData);
} else{
_tprintf(_T("g_pdwAnyData为NULL,不能读取数据!\n"));
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
}

void FreeData()
{
EnterCriticalSection(&g_cs);
__try{
_tprintf(_T("线程[ID:0x%X] Free Data\n\t"), GetCurrentThreadId());
if (NULL != g_pdwAnyData){
_tprintf(_T("g_pdwAnyData不为NULL,释放掉!\n"));
free(g_pdwAnyData);
g_pdwAnyData = NULL;
} else{
_tprintf(_T("g_pdwAnyData为NULL,不能释放!\n"));
}
}
__finally{
LeaveCriticalSection(&g_cs);
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: