本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。
关键段
关键段(Critical Section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。
下面的代码展示了Critical Section的使用方法:
const int COUNT = 10; int g_nSum = 0; CRITICAL_SECTION g_cs;//CRITICAL_SECTION struct DWORD WINAPI FirstThread(PVOID pvParam){ EnterCriticalSection(&g_cs);//Try enter critical section g_nSum = 0; for(int n = 1 ; n <= COUNT ; n++) g_nSum+=n; LeaveCriticalSection(&g_cs); return(g_nSum); } DWORD WINAPI SecondThread(PVOID pvParam){ EnterCriticalSection(&g_cs);//Try enter critical section g_nSum = 0; for(int n = 1 ; n <= COUNT ; n++) g_nSum+=n; LeaveCriticalSection(&g_cs); return(g_nSum); }
假如没有上面的EnterCriticalSection和LeaveCriticalSection,当两个线程函数分别在两个线程中执行的时候,g_nSum的状态是不可预计的。
在上面的代码中,首先定义了一个叫g_cs的CRITICAL_SECTION数据结构,然后把任何需要访问共享资源(这里的g_nSum)的代码放在EnterCriticalSection和LeaveCriticalSection之间。这里需要注意的是,关键段需要用在所有的相关线程中(即:上面的两个线程函数都要放在关键段中),否则共享资源还是有可能被破坏(只要对线程调度有清晰的认识就很容易理解其中的原因)。另外,在调用EnterCriticalSection之前需要调用InitializeCriticalSection初始化,当不需要访问共享资源的时候,应该调用DeleteCriticalSection:
/* Sample C/C++, Windows, link to kernel32.dll */ #include <windows.h> static CRITICAL_SECTION cs; /* This is the critical section object -- once initialized, it cannot be moved in memory */ /* If you program in OOP, declare this as a non-static member in your class */ /* Initialize the critical section before entering multi-threaded context. */ InitializeCriticalSection(&cs); void f() { /* Enter the critical section -- other threads are locked out */ EnterCriticalSection(&cs); /* Do some thread-safe processing! */ /* Leave the critical section -- other threads can now EnterCriticalSection() */ LeaveCriticalSection(&cs); } /* Release system object when all finished -- usually at the end of the cleanup code */ DeleteCriticalSection(&cs);
关键段工作原理
EnterCriticalSection会检查CRITICAL_SECTION中某些成员变量,这些成员变量表示是否有线程正在访问资源:
- 如果没有线程正在访问资源,那么EnterCriticalSection会更新成员变量,以表示调用线程已经获准对资源的访问,并立即返回,这样线程就可以继续执行。
- 如果成员变量表示调用线程已经获准访问资源,那么EnterCriticalSection会更新变量,以表示调用线程被获准访问的次数。
- 如果成员变量表示其他线程已经获准访问资源,那么EnterCriticalSection会使用一个事件内核对象把当前线程切换到等待状态。这样线程不会像前一篇讲的旋转锁(spinlock)那样耗费CPU。
关键段的核心价值在于它能够以原子的方式执行所有这些测试。另外TryEnterCriticalSection跟EnterCriticalSection一样拥有对共享资源的检测能力,但是不会阻塞调用线程。
关键段与旋转锁
关键段的另一个核心价值在于它可以使用旋转锁来对共享资源进行一定时间的“争用”,而不是立刻让线程进入等待状态、进入内核模式(线程从用户模式切换到内核模式大约需要1000个CPU周期)。因为,很多情况下共享资源不太会占用太长的时间,如果因为一个即将释放的共享资源而将线程切换到内核模式,将得不偿失。所以默认情况下在关键段阻塞线程之前,会多次尝试用旋转锁来“争用”共享资源,如果在这期间“争用”成功,那么EnterCriticalSection就会返回,代码将进入关键段执行;如果没有成功,则会将线程切换到等待状态。需要注意的是:只有在多核情况下才能够使关键段尝试这种特性。
为了在使用关键段的时候同时使用旋转锁,必须用如下函数来初始化关键段:
BOOL WINAPI InitializeCriticalSectionAndSpinCount( __out LPCRITICAL_SECTION lpCriticalSection, __in DWORD dwSpinCount );
下面的函数用以改变关键段的旋转次数:
DWORD WINAPI SetCriticalSectionSpinCount( __inout LPCRITICAL_SECTION lpCriticalSection, __in DWORD dwSpinCount );
关键段还可以和条件变量配合使用,这部分内容将在下一篇涉及。
更多关于关键段的内容可以参见:http://blog.csdn.net/morewindows/article/details/7442639
最后,设计一个简单的带一个缓冲队列的Log方法,要求线程安全,下面给出C++的实现:
void Log(int nLevel, const WCHAR* message) { struct DelayedLogInfo { int level; std::wstring message; }; static std::list<DelayedLogInfo> c_LogDelay; //log记录的缓冲队列 if (TryEnterCriticalSection(&g_CsLog)) //获得整个log的访问权限,如果失败则尝试在else里面获得对队列的访问权限 { EnterCriticalSection(&g_CsLogDelay);//读队列前,获得表示”队列“的变量的访问权限 while (!c_LogDelay.empty())//循环把队列中的东西全都写掉 { DelayedLogInfo& logInfo = c_LogDelay.front(); LogInternal(logInfo.level, logInfo.message.c_str()); c_LogDelay.erase(c_LogDelay.begin()); } LeaveCriticalSection(&g_CsLogDelay);//释放表示”队列“的变量的访问权限 //代码到这里释放了队列这个共享对象,因此,在下面这真正写入log时,其他试图写log的线程将只能向缓冲队列中写数据 // Log the message LogInternal(nLevel, message); LeaveCriticalSection(&g_CsLog); } else { EnterCriticalSection(&g_CsLogDelay); //写队列前,获得表示”队列“的变量的访问权限 DelayedLogInfo logInfo = {nLevel, message}; c_LogDelay.push_back(logInfo);//写队列 LeaveCriticalSection(&g_CsLogDelay);//释放表示”队列“的变量的访问权限 } }
劳动果实,转载请注明出处:http://www.cnblogs.com/P_Chou/archive/2012/06/20/critical-section-in-thread-sync.html