关键段是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”(指的是除了当前线程之外,没有其他任何线程会同时访问该资源)来对资源进行操控。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统不会调度任何想要访问统一资源的其他线程的。
首先看一段代码:
- DWORD WINAPI FirstThread(PVOID pvParam)
- {
- g_nSum = 0;
- for(int i = 0;i<1000;++i)
- {
- g_nSum = 0;
- for(int n=1; n<=Count; ++n)
- {
- g_nSum += n;
- }
- }
- cout<<"First:"<<g_nSum<<endl;
- return 0;
- }
- DWORD WINAPI SecondThread(PVOID pvParam)
- {
- g_nSum = 0;
- for(int i = 0;i<1000;++i)
- {
- g_nSum = 0;
- for(int n=1; n<=Count; ++n)
- {
- g_nSum += n;
- }
- }
- cout<<"Second:"<<g_nSum<<endl;
- return 0;
- }
我们可以用关键段来修改上述代码:
- #include <windows.h>
- #include <iostream>
- using namespace std;
- DWORD WINAPI FirstThread(PVOID pvParam);
- DWORD WINAPI SecondThread(PVOID pvParam);
- const long Count = 10000;
- long g_nSum = 0;
- CRITICAL_SECTION g_cs; //定义关键段结构
- int main()
- {
- if(InitializeCriticalSectionAndSpinCount(&g_cs,4000) != TRUE) //初始化关键段结构,同时使用旋转锁。
- {
- cout<<"初始化失败"<<endl;
- return 1;
- }
- HANDLE hThread1 = CreateThread(NULL,0,FirstThread,NULL,0,0);
- HANDLE hThread2 = CreateThread(NULL,0,SecondThread,NULL,0,0);
- CloseHandle(hThread1);
- CloseHandle(hThread2);
- Sleep(5000);
- DeleteCriticalSection(&g_cs); //释放关键段
- return 0;
- }
- DWORD WINAPI FirstThread(PVOID pvParam)
- {
- //需要使用共享资源的代码,放在EnterCriticalSection(&g_cs)和LeaveCriticalSection(&g_cs)之间
- EnterCriticalSection(&g_cs);
- g_nSum = 0;
- for(int i = 0;i<1000;++i)
- {
- g_nSum = 0;
- for(int n=1; n<=Count; ++n)
- {
- g_nSum += n;
- }
- }
- LeaveCriticalSection(&g_cs);
- cout<<"First:"<<g_nSum<<endl;
- return 0;
- }
- DWORD WINAPI SecondThread(PVOID pvParam)
- {
- EnterCriticalSection(&g_cs);
- g_nSum = 0;
- for(int i = 0;i<1000;++i)
- {
- g_nSum = 0;
- for(int n=1; n<=Count; ++n)
- {
- g_nSum += n;
- }
- }
- LeaveCriticalSection(&g_cs);
- cout<<"Second:"<<g_nSum<<endl;
- return 0;
- }
运行结果:
我们首先定义了一个名为g_cs的CRITICAL_SECTION数据结构然后把需要访问共享资源的代码放在EnterCriticalSection()和LeaveCriticalSection()函数之间。注意,在调用这两个函数的时候传入的是变量g_cs的地址。
需要注意的地方有:当我们有一个资源,或者多个总是在一起使用的资源,那么我们可以创建一个CRITICAL_SECTION结构来保护这些资源。如果有多个不总是在一起使用的资源,那么应该创建多个CRITICAL_SECTION结构。
关键段的优点是:容易使用,它们内部其实是用interlocked函数实现的,因此执行速度很快。但是无法用来在多个进程之间对线程进行同步。
一般情况下,我们会将CRITICAL_SECTION结构作为全局变量来分配,这样进程中的所有线程就能够非常方便的通过变量名来方访问这些结构。但是,CRITICAL_SECTION结构也可以作为局部变量来分配,或者从堆中动态分配,另外将他们作为类的一个私有字段来分配也是很常见的。在使用CRITICAL_SECTION结构的时候,只有两个必要条件:
(1)所有要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址。
(2)任何线程在试图访问被保护的资源之前,必须对CRITICAL_SECTION结构的内部成员进行初始化。进行初始化的函数是:
- void WINAPI InitializeCriticalSection(
- _Out_ LPCRITICAL_SECTION lpCriticalSection
- );
在对共享资源进行访问之前,需要调用下面函数,它会检查CRITICAL_SECTION结构中的成员变量,这些变量表示是否有线程正在访问资源,以及哪个线程正在访问资源:
- WINBASEAPI
- VOID
- WINAPI
- EnterCriticalSection(
- __inout LPCRITICAL_SECTION lpCriticalSection
- );
它会执行下列测试:
(1)如果没有线程正在访问资源,那么EnterCriticalSection会更新成员变量,以表示调用线程已经获准对资源的访问,并立即返回,这样线程就可以继续执行。
(2)如果成员变量表示调用线程已经获准访问资源,那么EnterCriticalSection会更新变量,以表示调用线程被获准访问的次数。
(3)如果成员变量表示其他线程已经获准访问资源,那么EnterCriticalSection会使用一个事件内核对象把当前线程切换到等待状态。这样线程不会像前一篇讲的旋转锁(spinlock)那样耗费CPU。系统会记住哪个线程想要访问这个资源,一旦当前正在访问资源的线程调用了LeaveCriticalSection,系统会自动更新CRITICAL_SECTION结构的成员变量并将等待中的线程切换回可调度状态。
注意,EnterCriticalSection函数是以原子方式执行这些测试。
另外一个可以执行和EnterCriticalSection函数相同功能的函数是:
- BOOL WINAPI TryEnterCriticalSection(
- _Inout_ LPCRITICAL_SECTION lpCriticalSection
- );
通过这个函数线程可以快速地检查是否能够访问某个共享资源。如果不能访问,那么它可以继续做些其他的事情,而不用等待。如果返回TRUE,那么CRITICAL_SECTION结构的成员变量已经更新过了,以表示该线程正在访问资源。因此每个返回值为TRUE的调用必须有一个对应的LeaveCriticalSection。
在代码完成对共享资源的访问后,应该调用下列函数:
- void WINAPI LeaveCriticalSection(
- _Inout_ LPCRITICAL_SECTION lpCriticalSection
- );
它同时会检查有没有其他线程由于调用了EnterCriticalSection函数而处于等待状态,如果有,那么函数会更新成员变量,把其中一个等待状态的线程切换回可调度状态。同样,它也是以原子方式执行所有操作。
如果线程不再需要访问共享资源的时候,可调用下列函数清理CRITICAL_SECTION结构:
- VOID DeleteCriticalSection (
- LPCRITICAL_SECTION lpCriticalSection // critical section
- );
关键段和旋转锁结合
当线程试图进入一个关键段,但是这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。这意味着线程必须从用户模式切换到内核模式(大约1000个CPU周期),这个切换的开销很大。
为了提高关键段的性能,Microsoft把旋转锁合并到了关键段中。因此,当调用EnterCriticalSection的时候,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问。只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。
为了在使用关键段的时候同时使用旋转锁,必须调用下面的函数来初始化关键段:
- BOOL WINAPI InitializeCriticalSectionAndSpinCount(
- _Out_ LPCRITICAL_SECTION lpCriticalSection,
- _In_ DWORD dwSpinCount
- );
上调用这个函数,那么函数会忽略这个参数,因此次数总是0.因为在单处理器上的机器上设置循环次数毫无意义:如果一个线程正在循环,那么占用资源的线程将没有机会放弃对资源的访问权。
代码示例:
- //关键段测试代码
- #include <iostream>
- #include <windows.h>
- using namespace std;
- CRITICAL_SECTION g_cs;//创建CRITICAL_SECTION结构全局变量
- int g_x = 20;
- //线程函数1
- DWORD WINAPI ThreadFunc1(LPVOID lpParam)
- {
- while(TRUE)
- {
- EnterCriticalSection(&g_cs);
- cout << "Thread1 get the access to resource" << endl;
- if(g_x > 10)
- {
- Sleep(1);
- cout<<"thread1 sell ticket : "<<g_x--<<endl;
- }
- else
- {
- cout << "Thread1 release the access to resource" << endl;
- LeaveCriticalSection(&g_cs);
- break;
- }
- cout << "Thread1 release the access to resource" << endl;
- LeaveCriticalSection(&g_cs);
- }
- return 0;
- }
- //线程函数2
- DWORD WINAPI ThreadFunc2(LPVOID lpParameter)
- {
- while(TRUE)
- {
- EnterCriticalSection(&g_cs);
- cout << "Thread2 get the access to resource" << endl;
- if(g_x > 0)
- {
- Sleep(1);
- cout<<"thread2 sell ticket : "<<g_x--<<endl;
- }
- else
- {
- cout << "Thread2 release the access to resource" << endl;
- LeaveCriticalSection(&g_cs);
- break;
- }
- cout << "Thread2 release the access to resource" << endl;
- LeaveCriticalSection(&g_cs);
- }
- return 0;
- }
- int main()
- {
- HANDLE hThread1;
- HANDLE hThread2;
- if(InitializeCriticalSectionAndSpinCount(&g_cs,10000) != TRUE) //初始化关键段结构,同时使用旋转锁。
- {
- cout<<"初始化失败"<<endl;
- return 1;
- }
- hThread1=CreateThread(NULL,0,ThreadFunc1,NULL,0,NULL);
- hThread2=CreateThread(NULL,0,ThreadFunc2,NULL,0,NULL);
- CloseHandle(hThread1);
- CloseHandle(hThread2);
- Sleep(4000);
- DeleteCriticalSection(&g_cs);
- return 0;
- }
