关键段
概述
- critical section是一小段代码,它在执行之前需要独占对一些共享资源的访问权。
这种方式可以让多行代码以“原子方式”来对资源进行操控。 - 这里的原子方式,是指除了当前线程之外,没有其他任何线程会同时访问该资源。
- 当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
const int COUNT = 10; int g_Sum = 0; CRITICAL_SECTION g_cs; DWORD WINAPI FirstThread(PVOID pvParam) { EnterCriticalSection(&g_cs); g_Sum = 0; for (int n = 1; n <= COUNT; n++) { g_Sum += n; } LeaveCriticalSection(&g_cs); return (g_Sum); } DWORD WINAPI SecondThread(PVOID pvParam) { EnterCriticalSection(&g_cs); g_Sum = 0; for (int n = 1; n <= COUNT; n++) { g_Sum += n; } LeaveCriticalSection(&g_cs); return (g_Sum); } |
- 关键段的最大好处在于它们非常容易使用,而且它们在内部也使用了Interlocked函数,因此执行速度非常快。
关键段的最大缺点在于它们无法用来在多个进程之间对线程进行同步。
细节一
- 一般,我们会将CRITICAL_SECTION结构作为全局变量来分配,这样进程中的所有线程就能够非常方便地通过变量名来访问这些结构。
但是,CRITICAL_SECTION也可以作为局部变量来分配,或者从堆中动态地分配,另外也可以将它们作为类的一个私有字段来分配。 - 使用CRITICAL_SECTION的时候,有两个必要条件:
- 所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION结构的地址。
- 在任何线程试图访问被保护的资源之前,都必须对CRITICAL_SECTION结构的内部成员进行初始化。
1 2 |
// 调用EnterCriticalSection之前,必须调用这个函数初始化 VOID InitializeCriticalSection(PCRITICAL_SECTION pcs); |
- 当线程不再需要共享资源的时候,应该调用DeleteCriticalSection清理结构。
这个函数会重置结构中的成员变量。
如果还有别的线程在使用这个关键段,删除的话,就会导致不可预料的结果。
1 |
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs); |
EnterCriticalSection
- 如果没有线程正在访问资源,那么该函数就会更新成员变量,以表示线程已经获准对资源的访问,并立即返回,这样线程就可以继续执行(访问资源)。
- 如果成员变量表示调用线程已经获准访问资源,那么该函数会更新变量,以表示调用线程被获准访问的次数,并立即返回,这样线程就可以继续执行。
但是这样的情况比较少见,只有当线程在调用LeaveCriticalSection之前连续调用EnterCriticalSection两次以上才会发生。 - 如果成员变量表示有一个线程(调用线程之外的其他线程)已经获准访问资源了,那么该函数会使用一个事件内核对象来把调用线程切换到等待状态。
而系统会记住这个线程想要访问的资源,一旦当前正在访问资源的线程调用了LeaveCriticalSection,系统会自动更新CRITICAL_SECTION的成员变量并将等待中的线程切换回可调度状态。
TryEnterCriticalSection
- 这个函数从来不会让调用线程进入等待状态,它会通过返回值来表示调用线程是否获准访问资源。
- 当这个函数发现资源正在被其他线程访问,那么它就会返回FALSE,其他情况,则返回TRUE。
- 通过这个函数,线程可以快速检查它是否能够访问某个共享资源。
如果不能访问的话,就继续做其他事情,而不用等待。 - 需要注意的是,每个返回TRUE的TryEnterCriticalSection调用必须有一个对应的LeaveCriticalSection。
1 |
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs); |
LeaveCriticalSection
- 该函数会检查结构内部的成员变量并将计数器减少1,该计数器用来表示调用线程获准访问资源的次数。
- 如果计数器大于0,LeaveCriticalSection会直接返回,不执行其他任何操作。
- 如果计数器变成了0,LeaveCriticalSection会更新成员变量,以表示没有任何线程正在访问被保护的资源。
- 它同时会检查有没有其他线程由于调用了EnterCriticalSection而处于等待状态。
- 如果至少有一个线程处于等待状态,那么函数会更新成员变量,把其中一个处于等待状态的线程切换回可调度状态。
- LeaveCriticalSection从来不会把线程切换到等待状态,它总是立即返回。
关键段和旋转锁
- 当线程试图进入一个关键段,但是这个关键段正被另一个线程占用的时候,函数会立即把调用线程切换到等待状态。
- 这意味着线程必须从用户模式切换到内核模式(大约是1000个CPU周期),这个切换的开始是很大的。
- 在多处理器的机器上,当前占用资源的线程可能在另一个处理器上运行,而且可能很快就会结束对资源的访问。
也就是说,有可能在需要等待的线程完全切换到内核模式之前,这个占用资源的线程就已经释放了该资源。
这种情况,会浪费大量的CPU时间。 - 为了提高关键段的性能,Microsoft把选择锁合并到了关键段中:
- 当调用EnterCriticalSection的时候,会用一个旋转锁不断的循环,尝试在一段时间内获得对资源的访问权,只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。
- 为了使用关键段的时候同时使用旋转锁,我们要调用InitializeCriticalSectionAndSpinCount这个函数。
InitializeCriticalSectionAndSpinCount
1 2 |
DWORD InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpinCount); |
- 第一个参数是关键段结构的地址,第二个参数是我们希望旋转锁循环的次数。
- 这个函数在单核处理器的机器上会忽略第二个参数。
SetCriticalSectionSpinCount
1 2 |
DWORD SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpinCount); |
- 用来改变关键段的旋转次数。
- 在单核处理器的机器上会忽略第二个参数。
- 用来保护进程堆的关键段所使用的旋转次数大约是4000。
关键段和错误处理
- 有一种情况是EnterCriticalSection函数会失败,可能会失败的原因是这个函数会分配一块内存,如果内存分配失败,函数就会抛出STATUS_NO_MEMORY的异常。
- 用结构化异常处理来捕获错误。
- 选择使用InitializeCriticalSectionAndSpinCount来创建关键段,并将dwSpinCount参数的最高位设为1。
这个参数的最高位设为1的话,就会在初始化的时候创建一个与关键段相关联的事件内核对象。如果无法创建就会返回FALSE。如果成功创建了,意味着EnterCriticalSection总是能正常的工作。
- 有三种情况下需要使用上面第二种做法:
- 我们不能接受调用EnterCriticalSection失败
- 我们知道争夺现象一定会发生
- 我们预计会在内存不足的环境下运行
- 关于争夺现象:
Windows XP以前,在内存不足的情况下,可能会发生争夺关键段的现象,这是系统可能无法创建所需的事件内核对象。 - 需要注意的是,Windows XP之后,引入了新的有键事件类型的内核对象,用来帮助解决在资源不足的情况下创建事件的问题。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Windows 核心编程 _ 内核对象:线程同步一07/29
- ♥ Windows 核心编程 _ 进程四06/25
- ♥ Windows高级调试_调试器03/19
- ♥ breakpad记述:Windows下静态库的编译使用03/15
- ♥ Soui应用 动画一06/24
- ♥ 关于多字节和宽字节一11/10