《Windows核心编程》第8章 用户模式下的线程同步

来源:互联网 发布:连云港房地产数据 编辑:程序博客网 时间:2024/05/29 15:06

在以下两种基本情况下,线程之间需要相互通信:
l  需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性。
l  一个线程需要通知其他线程某项任务已经完成。

原子访问指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。

Interlocked 系列的函数会以原子方式来操控一个值。比如如果想以原子方式给一个值加1 ,可以使用InterlockedIncrement 函数。注意,必须确保传给这些函数的变量地址是经过对齐的,否则这些函数可能会失败。(C 运行库提供了一个_aligned_malloc 函数,利用它来分配一块对齐过的内存。)Interlocked 系列函数执行极快,而且不用在用户模式和内核模式之间进行切换。

当多个进程需要对访问一个共享内存段(比如内存映射文件)中的值进行同步时,也可以使用Interlocked 函数。

当CPU 从内存中读取一个字节时,通常是取回一个高速缓存行。高速缓存行存在的目的是为了提高性能。虽然高速缓存行能提高性能,但在多处理器的机器上有可能损伤性能。我们应该根据高速缓存行的大小来将应用程序的数据组织到一起,并将数据与缓存行的边界对齐。这样做是为了确保不同的CPU 能够各自访问不同的内存地址,而且这些地址不在同一个高速缓存行中。

可以通过调用GetLogicalProcessorInformation 函数,来得到CPU 高速缓存行的大小。然后我们可以使用C/C++ 编译器的__declspec(align(#)) 指示符来对字段对齐加以控制。

一个通用规则是:我们既不应该使用旋转锁,也不应该进行轮询,而应该调用函数把线程切换到等待状态,直到线程想要访问的资源可供使用为止。

使用关键字volatile 的目的是告诉编译器,被它修饰的变量可能会被应用程序之外的其他东西修改,编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值。注意,给一个结构加上volatile 限定符等于给结构中所有的成员都加上了volatile 限定符,这样可以确保任何一个成员始终都是从内存中读取的。(传一个变量的地址给函数,那么函数必须从内存中读取它的值,编译器的优化程序不会对此产生影响。)

关键段critical section 是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。关键段最大的缺点是它们无法用于在多个进程之间对线程进行同步。

使用关键段要有两个必要条件:1 )所有想要访问资源的线程必须知道用来保护资源的CRITICAL_SECTION 结构的地址;2 )在任何线程视图访问被保护的资源之前,必须对CRITICAL_SECTION 结构的内部成员进行初始化,即调用函数InitializeCriticalSection 。当线程不再需要访问共享资源时,应该调用DeleteCriticalSection 来清理它。

可以使用TryEnterCriticalSection 来代替EnterCriticalSection 。前者从来不会让调用线程进入等待状态。它会通过返回值来表示调用线程是否获准访问资源。如果发现资源正被其他线程访问,那么它会返回FALSE 。否则,它会返回TRUE 。通过使用这个函数,线程可以快速地检查它是否能访问某个共享资源。如果不能访问共享资源,那么线程可以继续做其他的事情,而不用等待。返回TRUE 的TryEnterCriticalSection 调用必须有一个对应的LeaveCriticalSection 。

线程切换到等待状态,意味着线程必须从用户模式切换到内核模式,这个切换的开销非常大。为了提高关键段的性能,微软把旋转锁合并到了关键段中。当调用EnterCriticalSection 时,它会用一个旋转锁不断地循环,尝试在一段时间内获得对资源的访问权。只有尝试失败的时候,线程才会切换到内核模式并进入等待状态。我们可以通过调用函数
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs,DWORD dwSpinCount) 来初始化关键段,
第二个参数是我们希望旋转锁循环的次数。对于单处理器,此参数总是0 。

如果有两个或多个线程在同一时刻争夺同一个关键段时,那么在关键段内部会使用一个事件内核对象。这个事件内核对象只有在调用DeleteCriticalSection 的时候,系统才会释放。

WindowsXP 之前,在内存不足的情况下,可能会发生争夺关键段的现象,系统可能无法创建所需的事件内核对象。此时EnterCriticalSection 会抛出EXCEPTION_IINVALID_HANDLE 异常。处理方式有两种:1 )使用结构化异常处理来捕获错误。2 )使用InitializeCriticalSectionAndSpinCount 来创建关键段,并将参数dwSpinCount 的最高位设置为1 。此时该函数会在初始化时就创建一个与关键段相关联的事件内核对象。如果无法创建时间内核对象,函数返回FALSE 。

SRWLock 允许我们区分哪些想要读取资源的值的线程(读取者线程)和想要更新资源的值的线程(写入者线程)。让所有读取者线程同一时刻访问共享资源是可以的,因为仅是读取资源的值不会存在破坏数据风险。只有当写入者线程想要对资源进行更新时才需要进行同步。这种情况下,写入者线程应该独占对资源的访问权:任何其他线程,无论是读取者线程还是写入者线程,都不允许访问资源。写入者线程可以调用相关函数如下:
VOID InitializeSRWLock(PSRWLOCK SRWLock);// 初始化SRWLOCK 结构
VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);// 尝试获得被保护资源的独占访问权
VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);// 解除对资源的锁定
而读取者线程调用的函数则如下:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);

如果希望在线程同步的应用程序中得到最佳性能,首先应该尝试不要共享数据,然后依次使用volatile 读取,volatile 写入,Interlocded API ,SRWLock 以及关键段。当所有这些都不能满足要求时,最后考虑使用内核对象。

如果想让线程以原子方式把锁释放并将自己阻塞,直到某一个条件成立为止,可以使用函数
SleepConditionVariableCS 或SleepConditionVariableSRW 。当另一个线程检测到相应的条件已经满足时,比如存在一个元素可以让读取者线程读取,或者有足够的空间让写入者线程插入新元素,它会调用WakeConditionVariable 或WakeAllConditionVariable ,这样阻塞在Sleep* 函数中的线程就会被唤醒。

有用窍门和技巧
1) 以原子方式操作一组对象时使用一个锁
应用程序中的每个逻辑资源都应该有自己的锁,用来对逻辑资源的部分和整体访问进行同步。我们不应该为所有逻辑资源都创建单独的锁,因为如果多个线程访问的是不同的逻辑资源,这样会降低可伸缩性:任何时刻系统只允许一个线程执行。
2) 同时访问多个逻辑资源
访问多个资源的时,一般应在代码中的任何地方都要以完全相同的顺序来获得资源的锁,否则可能会发生死锁的情况。
3) 不要长时间占用锁
锁被长时间占用,其他线程可能进入等待状态,这样会影响应用程序的性能。

原创粉丝点击