概述:浪费CPU时间是件非常糟糕的事情,所以在实现用户模式下的线程同步时,我们需要一种机制,它既能让线程等待共享资源的访问权,又不会浪费CPU时间。
实现原理:
当线程想要访问一个共享资源或者想要得到一些“特殊事件”的通知时,线程必须调用操作系统的一个函数,并将线程正在等待的东西作为参数传入。如果
操作系统检测到资源已经可供使用了,或者特殊事件已经发生了,那么这个函数会立即返回,这样线程将仍然保持可调度状态。(线程可能并不会立即运行,它是可调度的,系统会根据一定的规则来给它分配CPU时间。)
如果无法取得对资源的访问权,或者特殊事件尚未发生,那么
系统会将线程切换到等待状态,使线程变得不可调度,从而避免了让线程浪费CPU时间。当线程在等待的时候,系统会充当它的代理。系统会记住线程想要访问什么资源,当资源可供使用的时候,它会自动将线程唤醒——线程的执行与特殊事件是同步的。
1. 关键段
关键段(critical section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。
这里的
原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。
当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。
定义关键段类型的变量:
// 定义一个 CRITICAL_SECTION 类型的变量
CRITICAL_SECTION cs;
当我们有一个资源要让多个线程访问的时候,应该创建一个 CRITICAL_SECTION 结构。
一个比喻:一个 CRITICAL_SECTION 结构就像是飞机上的一个卫生间,而马桶则是我们想要保护的数据。由于卫生间很小,因此在同一时刻只允许一个人(线程)在卫生间(关键段)内使用马桶(被保护的资源)。
关键段的最大缺点在于它们无法用来在多个进程之间对线程进行同步。
关键段细节:
使用 CRITICAL_SECTION 结构的两个必要条件
:
第一个条件是所有想要访问共享资源的线程必须知道用来保护资源的 CRITICAL_SECTION 结构的地址。
第二个条件是在任何线程试图访问被保护的资源之前,必须对 CRITICAL_SECTION 结构的内部成员进行初始化。
下面的函数用来对结构进行初始化:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
在调用 EnterCriticalSection 之前必须调用这个函数。
当知道线程将不再需要访问共享资源的时候,我们应该调用下面的函数来清理 CRITICAL_SECTION结构:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
在对共享资源进行访问之前,必须在代码中先调用下面的函数:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
EnterCriticalSection 会检查(CRITICAL_SECTION)结构中的成员变量,这些变量表示是否有线程正在访问资源,以及那个线程正在访问资源。
EnterCriticalSection 会执行下面的测试:
如果没有线程正在访问资源,那么 EnterCriticalSection 会更新成员变量,以表示调用线程已经获准对资源的访问,并立即返回,这样线程就可以继续执行(访问资源)。
如果成员变量表示调用线程已经获准访问资源,那么 EnterCriticalSection 会更新变量,以表示调用线程被获准访问的次数,并立即返回,这样线程就可以继续执行。这种情况非常少见,只有当线程在调用 LeaveCriticalSection 之前连续调用 EnterCriticalSection 两次以上才会发生。
如果成员变量表示有一个(调用线程之外的其他)线程已经获准访问资源,那么 EnterCriticalSection 会使用一个事件内核对象来把调用线程切换到等待状态。系统会记住这个线程想要访问这个资源,一旦当前正在访问资源的线程调用了 LeaveCriticalSection,系统会自动更新 CRITICAL_SECTION 的成员变量并将等待中的线程切换到可调度状态。
EnterCriticalSection 的内部并不怎么复杂,它只不过是执行了一些简单的测试。这个函数的价值在于它能够以原子方式执行所有这些测试。
TryEnterCriticalSection 函数:
TryEnterCriticalSection 从来不会让调用线程进入等待状态。它会通过返回值来表示调用线程是否获准访问资源。因此,如果 TryEnterCriticalSection 发现资源正在被其他线程访问,那么它会返回 FALSE。其他情况下,它会返回 TRUE。
通过使用这个函数,线程可以快速地检查它是否能够访问某个共享资源。如果不能访问共享资源,那么它可以继续做些其他的事情,而不用等待。如果 TryEnterCriticalSection 返回 TRUE,那么 CRITICAL_SECTION 的成员变量已经更新过了,以表示该线程正在访问资源。因此,每个返回值为 TRUE 的 TryEnterCriticalSection 调用必须有一个对应的 LeaveCriticalSection。
可以使用 TryEnterCriticalSection 函数来代替 EnterCriticalSection。
在代码完成对共享资源的访问后,应该调用下面的函数:
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
LeaveCriticalSection 会检查结构内部的成员变量并将计数器减1,该计数器用来表示调用线程获准访问共享资源的次数。如果计数器大于0,LeaveCriticalSection 会直接返回,不执行任何其他操作。
如果计数器变成了0,LeaveCriticalSection 会更新成员变量,以表示没有任何线程正在访问被保护的资源。它同时会检查有没有其他线程由于调用了 EnterCriticalSection 而处于等待状态。如果至少有一个线程处于等待状态,那么函数会更新成员变量,把其中一个处于等待状态的线程切换回可调度状态。
与 EnterCriticalSection 相似,LeaveCriticalSection 会以原子方式执行所有的测试和更新操作。但是,LeaveCriticalSection 从来不会把线程切换到等待状态,它总是立即返回。