CS我为什么要引入CRITICAL_SECTION和临界区呢?因为临界区是CRITICAL_SECTION的本质理论。
CS概述:
关键段(Critical Section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的
前面说了那么多也就是想引出我下面的CRITICAL_SECTION和临界区
什么是CRITICAL_SECTION呢?
在Windows.h中是这样一步步展现出来的
1
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
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;
各个参数的解释如下:
第一个参数:PRTL_CRITICAL_SECTION_DEBUGDebugInfo;
调试用的。此字段包含一个指针,指向系统分配的伴随结构,该结构的类型为
RTL_CRITICAL_SECTION_DEBUG。这一结构中包含更多极有价值的信息,也定义于 WINNT.H 中。我们稍后将对其进行更深入地RecursionCount,此字段包含所有者线程已经获得该临界区的次数。如果该数值为零,下一个尝试获取该临界区的线程将会成功。
第二个参数:LONGLockCount;
初始化为-1,n表示有n个线程在等待。
这是临界区中最重要的一个字段。它被初始化为数值 -1;此数值等于或大于 0
时,表示此临界区被占用。当其不等于 -1 时,OwningThread 字段(此字段被错误地定义于 WINNT.H 中 — 应当是 DWORD 而不是
HANDLE)包含了拥有此临界区的线程 ID。此字段与 (RecursionCount -1) 数值之间的差值表示有多少个其他线程在等待获得该临界区。
第三个参数:LONGRecursionCount;
表示该关键段的拥有线程对此资源获得关键段次数,初为0。
第四个参数:HANDLEOwningThread;
即拥有该关键段的线程句柄,微软对其注释为——from the thread's ClientId->UniqueThread
第五个参数:HANDLELockSemaphore;
实际上是一个自复位事件。
此字段的命名不恰当,它实际上是一个自复位事件,而不是一个信号。它是一个内核对象句柄,用于通知操作系统:该临界区现在空闲。操作系统在一个线程第一次尝试获得该临界区,但被另一个已经拥有该临界区的线程所阻止时,自动创建这样一个句柄。应当调用
DeleteCriticalSection(它将发出一个调用该事件的 CloseHandle 调用,并在必要时释放该调试结构),否则将会发生资源泄漏。
第六个参数:DWORDSpinCount;
旋转锁的设置,单CPU下忽略
仅用于多处理器系统。MSDN 文档对此字段进行如下说明:“在多处理器系统中,如果该临界区不可用,调用线程将在对与该临界区相关的信号执行等待操作之前,旋转 dwSpinCount 次。如果该临界区在旋转操作期间变为可用,该调用线程就避免了等待操作。”旋转计数可以在多处理器计算机上提供更佳性能,其原因在于在一个循环中旋转通常要快于进入内核模式等待状态。此字段默认值为零,但可以用
InitializeCriticalSectionAndSpinCount API 将其设置为一个不同值。
由这个结构可以知道关键段会记录拥有该关键段的线程句柄即关键段是有“线程所有权”概念的。事实上它会用第四个参数OwningThread来记录获准进入关键区域的线程句柄,如果这个线程再次进入,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。
因此可以将关键段比作旅馆的房卡,调用EnterCriticalSection()即申请房卡,得到房卡后自己当然是可以多次进出房间的,在你调用LeaveCriticalSection()交出房卡之前,别人自然是无法进入该房间。
回到这个经典线程同步问题上,主线程正是由于拥有“线程所有权”即房卡,所以它可以重复进入关键代码区域从而导致子线程在接收参数之前主线程就已经修改了这个参数。所以关键段可以用于线程间的互斥,但不可以用于同步。
另外,由于将线程切换到等待状态的开销较大,因此为了提高关键段的性能,Microsoft将旋转锁合并到关键段中,这样EnterCriticalSection()会先用一个旋转锁不断循环,尝试一段时间才会将线程切换到等待状态。下面是配合了旋转锁的关键段初始化函数
函数功能:初始化关键段并设置旋转次数
函数原型:
BOOLInitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
函数说明:旋转次数一般设置为4000。
函数功能:修改关键段的旋转次数
函数原型:
DWORDSetCriticalSectionSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
以下各段对每个字段进行说明。
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;
这一结构由 InitializeCriticalSection 分配和初始化。它既可以由 NTDLL
内的预分配数组分配,也可以由进程堆分配。RTL_CRITICAL_SECTION
的这一伴随结构包含一组匹配字段,具有迥然不同的角色:有两个难以理解,随后两个提供了理解这一临界区链结构的关键,两个是重复设置的,最后两个未使用。
下面是对 RTL_CRITICAL_SECTION 字段的说明。
Type 此字段未使用,被初始化为数值 0。
前面的1和2 是CRITICAL_SECTION的完整定义!现在我们就开始来细细的解密这个完整的定义
我们很多时候都在使用这个临界区,如果你也比较了解,也没必要继续看了,可以直接跳过本小段(如果不是很熟悉,那么将为后文打下一个好基础)!
临界区是一种防止多个线程同时执行一个特定代码节的机制,这一主题并没有引起太多关注,因而人们未能对其深刻理解。在需要跟踪代码中的多线程处理的性能时,对 Windows 中临界区的深刻理解非常有用。本文深入研究临界区的原理,以揭示在查找死锁和确认性能问题过程中的有用信息。它还包含一个便利的实用工具程序,可以显示所有临界区及其当前状态。
临界区是一种轻量级机制,在某一时间内只允许一个线程执行某个给定代码段。通常在修改全局数据(如集合类)时会使用临界区。事件、多用户终端执行程序和信号量也用于多线程同步,但临界区与它们不同,它并不总是执行向内核模式的控制转换,这一转换成本昂贵。稍后将会看到,要获得一个未占用临界区,事实上只需要对内存做出很少的修改,其速度非常快。只有在尝试获得已占用临界区时,它才会跳至内核模式。这一轻量级特性的缺点在于临界区只能用于对同一进程内的线程进行同步。
在将临界区传递给 InitializeCriticalSection
时(或者更准确地说,是在传递其地址时),临界区即开始存在。初始化之后,代码即将临界区传递给 EnterCriticalSection 和
LeaveCriticalSection API。一个线程自 EnterCriticalSection 中返回后,所有其他调用
EnterCriticalSection 的线程都将被阻止,直到第一个线程调用 LeaveCriticalSection
为止。最后,当不再需要该临界区时,一种良好的编码习惯是将其传递给 DeleteCriticalSection。
在临界区未被使用的理想情况中,对 EnterCriticalSection
的调用非常快速,因为它只是读取和修改用户模式内存中的内存位置。否则(在后文将会遇到一种例外情况),阻止于临界区的线程有效地完成这一工作,而不需要消耗额外的
CPU
周期。所阻止的线程以内核模式等待,在该临界区的所有者将其释放之前,不能对这些线程进行调度。如果有多个线程被阻止于一个临界区中,当另一线程释放该临界区时,只有一个线程获得该临界区。
函数原型:
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数说明:定义关键段变量后必须先初始化。
函数功能:销毁
函数原型:
void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数说明:用完之后记得销毁。
函数功能:进入关键区域
函数原型:
void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函数说明:系统保证各线程互斥的进入关键区域。
函数功能:离开关关键区域
函数原型:
void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
期待将持续更新(将说明CRITICAL_SECTION的参数含义和临界区的联系)!