临界区的实现原理

原创
2012/10/01 14:46
阅读数 2.5K

临界区概述:

用于多线程的互斥访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入临界区后,其他试图访问的线程将被挂起,直到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到对临界区的互斥访问。(临界区中一般都是一个简短的代码段)
在WINDOWS中,临界区是一种应用层的同步对象,非内核对象。并且临界区优先采用自旋的方式进行抢占


临界区API:

临界区初始化以及删除:

InitializeCriticalSection()
DeleteCriticalSection()

临界区两个操作原语: 

EnterCriticalSection()
LeaveCriticalSection()

临界区数据结构:

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;


_RTL_CRITICAL_SECTION_DEBUG数据结构
typedef struct _RTL_CRITICAL_SECTION_DEBUG {
    WORD   Type;
    WORD   CreatorBackTraceIndex;
    struct _RTL_CRITICAL_SECTION *CriticalSection;
    LIST_ENTRY ProcessLocksList;
    DWORD EntryCount;
    DWORD ContentionCount;
    DWORD Flags;
    WORD   CreatorBackTraceIndexHigh;
    WORD   SpareWORD  ;
} RTL_CRITICAL_SECTION_DEBUG, *PRTL_CRITICAL_SECTION_DEBUG, RTL_RESOURCE_DEBUG, *PRTL_RESOURCE_DEBUG;
(代码来自VS2005 WINNT.h)


_RTL_CRITICAL_SECTION 各字段解释: 
DebugInfo:指向一个调试用的数据,该结构的类型为RTL_CRITICAL_SECTION_DEBUG
LockCount: 初始值-1,若结果大于等于0,表示该临界区已被线程占用。
OwningThread: 当前拥有临界区的线程 
RecursionCount:所有者线程连续进入临界区的次数 
LockSemaphore: 内核对象句柄,用于告知操作系统,该临界区目前处于空闲状态,用于唤醒因等待临界区而挂起的线程
SpinCount:MSDN对该字段做如下解释:
"On multiprocessor systems, if the critical section is unavailable, the calling thread will spin 
dwSpinCount times before performing a wait operation on a semaphore associated with the critical 
section. If the critical section becomes free during the spin operation, the calling thread 
avoids the wait operation."

在多处理器系统中,如果临界区已被占用,那么线程就自旋SpinCount次去获取临界区,而不是通过阻塞等待的方式去获取临界区。如果在自旋的过程中临界区空间,就可以直接进入临界区,减少等待时间(如果进入等待状态,需要用户态内核态的切换,代价较大)。主要意思就是为了提高效率,下面我们会分析什么叫自旋。

 

临界区API实现过程 

InitialzeCriticalSection

在初始化的过程中,会测试CPU的数量,若CPU数量为1,则忙等待没有意义。则SpinCount=0,
若CPU数量大于1,则设置SpinCount,在进入临界区时,会采取主动进入策略。

 EnterCriticalSection

1. 若临界区还未被占用,则更新临界区数据结构,表示调用线程已经获得访问临界区的权限,返回。
2. 若线程在已经获取访问权限的情况下,再次EnterCriticalSection,则更新线程获取访问的次数(即连续Enter的次数)。
3. 若临界区已经其他线程占用,则当前线程 通过SpinCount来控制忙等的次数,在SpinCount已经等于0还没有获得临界区对象的情况下,函数直接通过临界区对象内部的事件对象进行等待(等待及唤醒涉及到用户态和内核态的切换,不是最优方案,优先采用自旋的方式进入临界区)。忙等待是通过对LockCount进行原子读写操作实现。

可以在如下网址看具体汇编代码:来源:(http://blog.csdn.net/joeleechj/article/details/7081124)

 RtlLeaveCriticalSection

1. _RTL_CRITICAL_SECTION数据结构中相关标志位设置 ,比如RecursionCount--,如果为0,表示没有线程占用临界区
2. 将当前占有线程句柄设为0,表示现在临界区目前处于有信号状态,可以被获取
3. 若有其他线程在等待,唤醒等待线程

 

CRITICAL_SECTION和CRITICAL_SECTION_DEBUG之间的关系
http://msdn.microsoft.com/zh-cn/magazine/cc164040(en-us).aspx)

通过该数据结构可以发现,在进程中,所有的临界区的DEBUG信息通过链表进行串接。在已知某个临界区对象的
情况下, 通过链表数据结构,可以访问到数据的临界区对象。

自旋:

对于临界区的操作,(EnterCriticalSection)操作,采用的是主动进入临界区。意思就是,当没有能够进入时,不停的主动尝试进入,直至进入为止。这种主动进入的方式,称为自旋(也叫忙等待)。
被动方式:获取不到后,进入等待队列,当要获取的对象被释放后,系统唤醒等待的线程,这个方式叫做被动方式。

小结:

1.进入灵界区和离开临界区是成对操作,进入临界区必须要有离开临界区否则临界区保护的共享资源将永远不会被释放。

2.在使用临界区时,临界区间使用的代码最好简短,减少其他线程的等待时间,提高程序性能。

3.临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。

4. 临界区是用户态下的对象,非内核对象,所以在使用时无需再用户态和内核态之间切换,效率明显要比其他用户互斥的内核对象高。

 

参考:

http://msdn.microsoft.com/zh-cn/magazine/cc164040(en-us).aspx

展开阅读全文
打赏
1
3 收藏
分享
加载中
更多评论
打赏
0 评论
3 收藏
1
分享
返回顶部
顶部