使用临界区对象(CriticalSection)需要注意的一些事情

1. 临界区对象不是内核对象,因此不能继承,不能跨进程,也不能用waitfor什么的函数来限定时间等待。这个很好理解,你想想WaitFor要求传一个句柄,而临界区对象的类型都不是句柄,也不能用CloseHandle来关闭,怎么可能会能让WaitForXXX搞了。

2. 临界区对象使用前必须初始化,不初始化会崩溃,这是我的亲历。

3. 线程进入临界区之前会先自旋那么几次,所有自旋锁都失败了之后会创建一个内核对象然后等待这个内核从而进入到内核模式。

4. Enter和Leave必须匹配,每Enter一次,就得Leave一次,这又是可恶的计数机制。参见下面的代码:

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    //  The following three fields control entering and exiting the critical
    //  section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

这是临界区对象的定义,看见
RecursionCount
这个对象了吧,你觉得它能干点啥?同时在这里你还能看到一个信号量的内核对象,还有一个自旋数量。这些玩意印证了上面的话。如果你同一个线程Leave之前Enter了两次,必须调用两个Leave,不然这个临界区对象依然会阻塞别的线程。再不明白就去看我前面有关挂起线程的那个博文。


5. 由于进入临界去是无限等待的,因此你有时间肯定希望有种方法能够查看一下临界区是否可用,不可用则希望线程立刻去做其它的事情。这时候,你就需要一个TryEnterCriticalSectionAPI,这玩意很好理解,你踹一脚临界区,如果能进去就进去,不能进去这个API立刻以False返回,你就可以安排线程去做其它的事情。注意:你一脚踹进去了之后完事了记得要离开(LeaveCriticalSection)


6. 前面说了,临界区真正用内核对象挂起线程之前会自旋好几次,因此你看对象里就有一个自旋锁的计数。你可以改这个自旋锁的数量。当然我不是说让你直接修改对象的成员变量!你可以在初始化的时候指定自旋锁的数量,用这个API:InitializeCriticalSectionAndSpinCount。在这里小说一下临界区为什么会自旋。因为程序从用户态转到内核模式需要昂贵的开销(大概数百个CPU周期),很多情况下,A线程还没完成从用户态转到内核态的操作呢,B线程就已经释放资源了。于是临界区就先隔一段时间自旋一次,直到所有自旋次数都耗尽,就创建个内核对象然后挂起线程。但是,如果您的机器只有一个CPU,那么这个自旋次数就没用了,操作系统直接会无视它。原因如下:你自旋着呢,操作B线程释放不了资源,于是你还不如直接切入等待状态让B来释放资源。动态更改自旋数量请使用SetCriticalSectionSpinCount,别做直接更改对象成员变量的二事!


7. 最后,初始化临界区和进入临界区的时候都有可能会遇到异常状况,比如初始化的时候需要申请一些内存来保存DEBUG的信息(参见上面代码的第一个成员变量),如果内存不够,初始化就崩了。进入临界区的时候可能需要新建一个内核对象,如果内存不够,也崩了。解决这个问题的方法有两种

  1. 结构化异常处理
  2. 初始化的时候使用InitializeCriticalSectionAndSpinCount。这个API有返回值,如果它申请DEBUG信息失败,返回FALSE,另外,刚才提到了这个API可以指定自旋锁的自旋次数。这个自旋次数的范围并非是用0到0xFFFF FFFF而是0--->0x00FF FFFF,因此你可以设定高位为1指示初始化的时候直接建立内核对象。如果建立失败,这个函数也会调用失败。当然了,一上来就搞一个内核对象似乎有点浪费内存,但是这样能够保证进入临界区不会失败!但是吧,你需要注意,设置高位来保证内核对象的创建只能在2000上玩。MSDN上有说明,不信你看:
  3. Windows  2000:  If the high-order bit is set, the function pre-allocates the event used by theEnterCriticalSection function. Pre-allocation guarantees that entering or leaving the critical section will not raise an exception in low memory conditions. Do not set this bit if you are creating a large number of critical section objects, because it consumes a significant amount of nonpaged pool. Note that this event is allocated on demand starting with Windows XP and the high-order bit is ignored.

最后是一些实验:

我们看看用InitializeCriticalSection初始化一个临界区对象后,这些成员变量(除去DEBUG外)都是什么样子。

我们Enter一下,看看会变成什么样子


我们再让其它线程也Enter一下看看


可见,新建了一个内核对象。

我们现在让主线程退出临界区

对照线程句柄我们可以看出第二个线程获得了临界区对象。

我们再让第二个线程退出临界区。

临界区除去内核对象外回到了原始状态。


实验2:我们让临界区对象在同一线程内相继被进入两次

::EnterCriticalSection(&g_cs);
::EnterCriticalSection(&g_cs);

可见,计数增加了一个,变成了,因此你得leave两次才能开锁

你可能感兴趣的:(Windows核心编程)