概述
- 当所有线程都能独自运行而不需要相互通信的时候,Microsoft Windows将进程最佳运行状态。
- 系统中的所有线程必须访问系统资源,比如堆、串口、文件、窗口以及无数其他资源。
如果一个线程独占了对某个资源的访问,那么其他线程就无法完成它们的工作。 - 以下两种基本情况下,线程间需要相互通信:
- 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性。
- 一个线程需要通知其他线程某项任务已完成。
原子访问:Interlocked系列函数
- 关于这些Interlocked函数如何工作的,取决于代码运行的CPU平台。
如果是X86系列CPU,那么Interlocked函数会在总线上维持一个硬件信号,这个信号会阻止其他CPU访问同一个内存地址。 - 我们必须确保传给这个函数的变量地址是经过对齐的,否则这些函数可能会失败。
- C运行库提供了一个_aligned_malloc函数,这个函数可以用来分配一块对齐过的内存。
1 2 3 4 |
// 1.目标值 // 2.增量 LONG InterlockedExchangeAdd(PLONG volatile_plAddend, LONG lIncrement); |
1 2 |
LONGLONG InterlockedExchangeAdd64(PLONGLONG volatile_pllAddend, LONG LONG llIncrement); |
1 2 3 4 5 6 7 8 9 10 11 |
// eg. long g_x = 0; DWORD WINAPI ThreadFunc1(PVOID pvParam) { InterlockedExchangeAdd(&g_x, 1); return 0; } DWORD WINAPI ThreadFunc2(PVOID pvParam) { InterlockedExchangeAdd(&g_x, 1); return 0; } |
1 2 3 4 5 6 7 8 9 10 11 |
// 其他系列函数 // 把第一个参数指向的内存的值,以原子方式替换为第二个参数的值 LONG InterlockedExchange(PLONG volatile_plTarget, LONG lValue); LONGLONG InterlockedExchange64(PLONGLONG volatile_plTarget, LONG lValue); // 把第一个参数指向的内存的值,以原子方式替换为第二个参数的值 PVOID InterlockedExchangePointer(PVOID* volatile_ppvTarget, PVOID PVValue); |
- 对于32位应用程序而言,InterlockedExchange和InterlockedExchangePointer是以一个32的值替换了32的值。
对64位应用程序而言,InterlockedExchange替换的是一个32位的值,而InterlockedExchangePointer替换的是64位的值。
这两个函数都会返会原来的值。 - 此外,必须保证变量和锁所保护的数据位于不同的高速缓存行中。
如果锁变量和数据共享同一高速缓存行,那么使用资源的CPU就会与任何试图访问该资源的CPU发生争夺,从而影响到性能。
1 2 3 4 5 6 7 8 9 10 11 12 |
// 一个旋转锁的实现 BOOL g_fResourceInUse = FALSE; void Func1() { while(InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE) Sleep(0); // access the resource // do something InterlockedExchange(&g_fResourceInUse, FALSE); } |
- 下面这个函数以原子的方式执行一个测试和设置操作。
对32位应用程序而言,这两个函数都是对32位的值进行操作。
对64位应用程序而言,InterlockedCompareExchange对32位值进行操作,而InterlockedCompareExchangePointer对64位值进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 交换函数 // 将plDestination当前值与lComparand的值进行比较: // 如果相等,将*plDestination修改为lComparand的值 // 如果不相等,*plDestination将保持不变,函数返回*plDestination原来的值。 // 注意:所有这些操作是作为一个原子来完成的。 PLONG InterlockedCompareExchange(PLONG plDestination, LONG lExchange, LONG lComparand); LONG InterlockedCompareExchangePointer(PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand); // 另外一个版本: 用来处理已对齐的64位值 LONG InterlockedCompareExchange64(PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand); |
- 从Windows XP开始,除了能对整数或布尔值进行这些原子操作外,还能用一系列其他的函数对一种被称为Interlocked单向链表的栈进行操作。
栈中的每个操作,如入栈或出栈,必定是以原子的方式进行的。
函数 | 描述 |
InitializeSListHead | 创建一个空栈 |
InterlockedPushEntrySList | 在栈顶添加一个元素 |
InterlockedPopEntrySList | 移除位于栈顶的元素并将它返回 |
InterlockedFlushSList | 清空栈 |
QueryDepthSList | 返回栈中元素的数量 |
高速缓存行
- 当CPU从内存中读取一个字节的时候,它并不只是从内存中取回一个字节,而是取回一个高速缓存行。
- 高速缓存行可能包含32个字节(老式CPU),64个字节,甚至是128个字节(具体取决于CPU),它们始终都对齐到32字节边界,64字节边界或128字节边界。
- 高速缓存行的存在的目的是为了提高性能。
一般来说,应用程序会对一组相邻的字节进行操作。如果所有字节都在高速缓存中,那CPU就不必访问内存总线,因为后者耗费的时间要多得多。 - 当一个CPU修改了高速缓存行中的一个字节时,机器中的其他CPU会收到通知,并使自己的高速缓存行作废。
- 意味着应该按照高速缓存行的大小来将应用程序的数据组织在一起,并将数据与缓存行的边界对齐。
目的是为了确保不同的CPU能够各自访问不同的内存地址,而且,这些内存地址不再同一个高速缓存行中。
此外,还应该把只读的数据(或者不经常读的数据),与可读可写的数据区分开来存放。
还应该把差不多会同一段时间访问的数据组织在一起。
缓存行的大小
- 调用Win32的GetLogicalProcessorInformation函数,可以返回一个SYSTEM_LOGICAL_PROCESSOR_INFORMATION的结构体数组。
- 在这个结构体数组里面,每个结构的Cache字段,是一个CACHE_DESCRIPTOR结构,这个结构里面的LineSize就表示的是CPU的高速缓存行的大小。
- 根据这个缓存行的大小,我们可以使用C/C++编译器的__declspec(align(#))指示符来对字段加以控制。
关于高级线程同步
- 当线程想要访问一个共享资源或想要得到一些“特殊”事件的通知时,线程必须调用操作系统的一个函数,并将线程正在等待的东西作为参数传入。
- 如果操作系统检测到,资源已经可供使用了,或者特殊事件已经发生了,那么这个函数会立即返回,这样线程将仍然保持可调度状态。
- 如果无法取得对资源的访问权,或者特殊事件尚未发生,那么系统会将线程切换到等待状态,使线程变得不可调度,从而避免了让线程浪费CPU时间。
- 当线程在等待的时候,系统会充当它的代理。系统会记住线程想要访问什么资源,当资源可供使用的时候,系统会自动将线程唤醒(线程的执行与特殊事件是同步的)。
volatile
- 这个限定符高速编译器不要对这个变量进行任何形式的优化,而是始终从变量在内存中的位置读取变量的值。
- 给一个结构体加volatile限定符,等于给结构中的每一个成员都加上volatile限定符。可以确保任何一个成员始终都是从内存中读取的。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ x86_64汇编学习记述二08/07
- ♥ COM组件_303/07
- ♥ Soui八06/20
- ♥ COM组件_101/31
- ♥ Windows 高级调试 _ 内存破坏03/21
- ♥ breakpad记述:Windows下静态库的编译使用03/15