要使用关键段首先需要定义CRITICAL_SECTION结构。然后把任何需要共享的代码放在EnterCriticalSection和LeaveCriticalSection之间。
如
Int g_a=0; CRITICAL_SECTION cs; DWORD WINAPI ThreadProc1(PVOID) { EnterCriticalSection(&g_a); for(int i=0;i<100;i++) g_a++; LeaveCriticalSection(&cs); return 0; } DWORD WINAPI ThreadProc2(PVOID) { EnterCriticalSection(&g_a); for(int i=0;i<100;i++) g_a++; LeaveCriticalSection(&cs); return 0; } |
作者使用了一个非常形象的比喻。CRITICAL_SECTION结构就像一个卫生间,卫生间内的区域就是要保护的资源。在同一时刻只允许一个人进入卫生间内。如果有多个区域需要保护可以分别定义多个CRITICAL_SECTION结构。调用EnterCriticalSection传入CRITICAL_SECTION结构的地址,这个结构标识要访问的保护资源,也就相当于卫生间。当一个人想上卫生间时,必须首先检查卫生间门上的占用标识,看是否有人占用。如果此时无人,那么EnterCriticalSection将允许调用线程进入卫生间。如果卫生间已有人占用,那么调用线程必须在门外等待。如果一个人使用过卫生间后,他必须把卫生间改为未占用状态。LeaveCriticalSection告诉系统它已经离开了所占用的资源。如果忘记调用LeaveCriticalSection,系统会一直让等待进入此卫生间的人在门外等待。
关键段由于在内部使用了Interlock系列函数因此执行速度非常快。它的缺点是不能在多进程之间对线程进行同步。而信号量和事件则可以。
一般情况下CRITICAL_SECTION结构会作为全局变量来分配,这样进程内的所有线程都可以通过该变量来访问这些结构。实际使用中将此结构作为局部变量、从堆中分配或者是类的私有成员也都是可以的。
但有两个必要条件:
第 一:想要访问资源的线程必须知道用来访问资源的CRITICAL_SECTION结构的地址。
第二:在任何线程访问被保护的资源之前,必须对CRITICAL_SECTION结构进行初始化。初始化调用:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs); |
此函数会设置CRITICAL_SECTION结构的一些成员。如果这些成员没有经过初始化,结果将是不可预料的。
当线程不需要访问共享资源时,应该调用以下函数来清理CRITICAL_SECTION结构:
<SPAN style="FONT-SIZE: 18px">VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);</SPAN> |
在访问共享资源调用EnterCriticalSection,该函数将执行下面的动作:
1:如果没有线程在访问资源,该函数会更新成员变量,表示线程已经获得对资源的访问权,同时更新调用线程被准许访问的次数。线程可以继续执行。
2:如果有其他线程占用资源,该函数会使用一个事件内核对象把调用线程切换到等待状态。当占用线程调用LeaveCriticalSection时,系统会自动更新CRITICAL_SECTION的成员变量并将挂起的线程切换到可调度状态。
为了防止当资源被其他线程占用时,主调线程被挂起,可以调用TryEnterCriticalSection函数,该函数将会检测此时主调线程是否可以访问共享资源。如果资源被占用该函数返回false。否则返回true。
如果检测到资源此时已被占用,主调线程这是可以做其他事情,而不是被挂起。由于TryEnterCriticalSection函数会更新CRITICAL_SECTION结构的某些成员,因此需要对应一个LeaveCriticalSection函数。