线程之间在以下两种情况下需要相互通信:
1. 多个线程在访问一个共享资源时保证资源不被破坏;
2. 一个线程在完成一个特定任务时需要通知其它线程。
一个线程在修改一个共享资源时保证其它线程不能同时修改这个资源。
LONG InterlockedExchangeAdd( PLONG volatile plAddend, LONG lIncement); LONGLONG InterlockedExchangedAdd64( PLONGLONG volatile pllAddend, LONG llIncrement); LONG InterlockedExchange( PLONG volatile plTarget, LONG lValue); LONGLONG InterlockedExchange64( PLONGLONG volatile pllTarget, LONGLONG llValue); PVOID InterlockedExchangePointer( PVOID* volatile ppvTarget, PVOID pvValue); PVOID InterlockedCompareExchange( PLONG plDestination, LONG lExchange, LONG lComparand); PVOID InterlockedCompareExchangePointer( PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand); LONGLONG InterlockedCompareExchange64( LONGLONG pllDestination, LONGLONG llExchange, LONGLONG llComparand);
以上这些函数在操作一个共享变量时不会被中断,也保证不会有两个线程同时修改这个共享变量。这些函数的实现依赖于 CPU 平台,对于 x86 CPU,互锁操作会向总线发出一个信号避免其它 CPU 访问相同的内存地址。
InterlockedExchange 非常适用于实现一个自旋锁,如下代码:
// 表示一个共享资源是否正在使用的全局变量 BOOL g_fResourceInUse = FALSE; void Func(){ // 如果共享资源正在被其它线程访问,InterlockedChanged 返回 TRUE,必须等待其它线程访问完成 while (InterlockedChange(&g_fResourceInUse, TRUE) == TRUE) Sleep(0); // 获得共享资源访问权 ... // 不再需要访问共享资源 InterlockedExchange(&g_fResourceInUse, FALSE); }
如果其它线程也会使用类似的代码,g_fResourceInUse 在变为 FALSE 之前,while 语句会一直循环执行。使用这种技术时要非常小心,因为自旋锁会非常消耗 CPU 资源,CPU 必须持续地对两个值进行比较直到另一个线程转变它的状态,同时这段代码假定所有使用自旋锁的线程都运行在相同的优先级中,你也必须禁止线程优先级自动提升。这个假定是必须的,否则可能会引起死锁。假定有两个线程 A 和 B,线程 A 的优先级要低,如果 A 线程先执行并获得了资源的访问权,这时 B 线程开始执行,因为它的优先级高于 A,它会抢先 A 线程的执行并在 while 语句一直测试 g_fResourceInUse 是否变为 FALSE,由于 Sleep 函数只是放弃它剩余的时间片,线程调度器会将执行权交给优先级高于或等于线程 B 的线程,如果没有其它符合条件的线程,B 线程还会接着执行,这样,线程 A 将不会有机会修改 g_fResourceInUse 的值。
另外,应该避免锁变量和它保护的资源在不同的 cache line 中,否则访问这个资源的 CPU 之间会相互影响,降低程序性能。也要避免在单 CPU 主机中使用自旋锁,因为当一个线程正在等待时,它会浪费宝贵的 CPU 时间,解决这个问题的方法是在循环中使用 Sleep 函数,可以给 Sleep 函数传递一个随机的时长,如果在一个循环中没有被获准访问共享资源,可以适当地增加休眠时间。自旋锁多用于多个处理器的主机中,当一个线程处于自旋状态时,其它线程可继续在其它 CPU 中运行,可是也要注意不要使一个线程处于自旋太长时间。
没有只用于读取一个值的互锁函数,因为没有必要,如果一个线程要读取一个处于互锁函数保护下的内容,它的值一定总是好的,你不知道读取的值是原始值还是更新的值,但你得到的内容一定是这两个值之一。
互锁函数也可用于多处理器线程中访问共享内存区域,如内存映射文件。
上面列出的原子操作函数主要操作整数或布尔类型的数据,下面列出操作栈的函数,叫做“单链表互锁”函数:
函数名称 | 描述 |
InitialzeSListHead | 创建一个空的栈 |
InterlockedPushEntrySList | 在栈顶增加一个元素 |
InterlockedPopEntrySList | 从栈顶移除一个元素,并返回它 |
InterlockedFlushSList | 清空栈 |
QueryDepthSList | 返回栈中元素数量 |
用于构建高性能的应用程序,当一个 CPU 从内存中读取一个字节时,它会一次从内存中获取足够长度的字节来填充 Cache Lines,这个 Cache Lines 一般有 32(比较老的 CPU)、64 或 128 字节长度,Cache lines 主要缘于应用程序大多操作相邻数据的这个思想,这个方法可以减少读取内存的次数,相应地提高了性能。可是这个方法也带来了一些问题,比如多个 CPU 同时修改一块内存,由于 Cache Lines 的存在,一个 CPU 对缓存内容的修改其它 CPU 不会感觉得到,解决这个问题的方法是如果当前 CPU Cache Lines 中包含了另一个 CPU 修改的数据,当前 CPU 的 Cache Lines 中数据立即失效,重新从内存中读取相应数据来填充它的 Cache Lines。
为了充分利用 Cache Lines 特性,在设计应用程序时应确保不同的 CPU 访问不同的至少由一个 Cache-Line 边界分隔的不同内存地址,还有将只读数据(或不频繁更新的数据)与读写数据分开。
你可以使用 C/C++ 编译器的 __declspec(align#)) 指令来控制变量的对齐,# 表示 Cache Lines 缓冲区的大小,可以使用 GetLogicalProcessorInformation 来获取。