第8章 用户模式下的线程同步(1)_Interlocked系列函数
8.1 原子访问:Interlocked系列函数(Interlock英文为互锁的意思)
(1)原子访问的原理
①原子访问:指的是一线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问该资源。
②从汇编的角度看,哪怕很简单的一条高级语言都可以被编译成多条的机器指令。在多线程环境下,这条语句的执行就可能被打断。而在打断期间,其中间结果可能己经被其他线程更改过,从而导致错误的结果。
③在Intelx86指令体系中,有些运算指令加上lock前缀就可以保证该指令操作的原子性。其原理是CPU执行该指令时发现其前面加lock前缀,就会在总线维持一个硬件信号以阻止其他CPU(或线程)访问与该指令相同的目标内存地址。(注意是指令目标操作数的内存地址而且这些地址是经过内存对齐过的!)
④以InterlockedIncrement函数为例分析原子访问的原理
LONG InterlockedIncrement(LPLONG volatile lpAddend)
{
_asm{ //xadd指令两个功能:①交换(opd)→(ops);②(opd)←(opd)+(ops)
mov eax, //前缀lock表示线程在执行该指令时,会将[ecx]内存锁
mov cx,Addend //定(实际上是在CPU总线上放一个信号)以标志该内存正在被使用,
lock xadd [ecx],eax //从而阻止其他线程同时访问该内存。即其他线程要么在该指令之前,
inc eax //要么在指令之后才能访问[ecx]指向的这块内存
}
}
(2)Windows内核支持的整数原子操作——Interlocked***互锁函数
|
函数 |
描述 |
|
InterlockedIncrement InterlockedDecrement |
对LONG变量加(减)1,如:InterlockedIncrement(&g_iX) (内部使用lock xadd指令) |
|
InterlockedExchangeAdd |
将一个值加到一个LONG变量,返回变量原值,使用lock xadd指令,如:int g_iX = 0; // InterlockedExchangeAdd(&g_iX,-2); //g_iX -= 2; |
|
InterlockedCompareExchange |
InterlockedCompareExchange( plDest,lExchange, lComperand)。比如*plDestination==lComperand,如果相等将*plDest修改为lExchange,如果不等,则*plDest不变。返回值为*plDest原来的值。 (使用lock cmpxchag指令) |
|
InterlockedExchange InterlockedExchangePointer |
将第1个参数所指的内存里的当前值,以原子方式替换为第2个参数指定的值。函数返回值为原始值。后面那个函数是改变一个指针本身的值。(如果xchg指令,虽不加lock。但默认为原子操作) |
|
InterlockedOr |
对一个LONG变量做逻辑或运算,使用lock or指令 |
|
InterlockedAnd |
对一个LONG变量做逻辑与运算,使用lock and指令 |
|
InterlockedXor |
对一个LONG变量做逻辑异或运算,使用lock xor指令 |
(3)Interlocked单向链表函数
|
函数 |
描述 |
|
InitializeSListHead |
创建一个空栈 |
|
InterlockedPushEntrySList |
在栈顶添加一个元素 |
|
InterlockedPopentrySList |
移除位于栈顶的元素并将它返回 |
|
InterlockedFlushSlist |
清空栈 |
|
QueryDepthSlist |
返回栈中元素的数量 |
【Interlocked单链表】演示程序

#include <windows.h>
#include <malloc.h>
#include <stdio.h>
#include <tchar.h> typedef struct _tag_PROGRAM_ITEM
{
SLIST_ENTRY ItemEntry;
ULONG Signature;
}PROGRAM_ITEM,*PPROGRAM_ITEM; int _tmain()
{
ULONG Count;
PSLIST_ENTRY pFirstEntry, pListEntry;
PSLIST_HEADER pListHead;
PPROGRAM_ITEM pProgramItem; //初始化链表头部
//C库函数_aigned_malloc用来分配一个块对齐过的内存,其中第1个参数表示要分配
//的字节数,第2个参数表示要对齐到的字节边界(必须是2的整数幂次方)
pListHead = (PSLIST_HEADER)_aligned_malloc(sizeof(SLIST_HEADER),
MEMORY_ALLOCATION_ALIGNMENT);
if (NULL == pListHead){
_tprintf(_T("内存分配失败!"));
return -;
} //初始化链表头,创建一个空栈
InitializeSListHead(pListHead); //插入10个元素
for (Count = ; Count <= ;Count++){
pProgramItem = (PPROGRAM_ITEM)_aligned_malloc(sizeof(PROGRAM_ITEM),
MEMORY_ALLOCATION_ALIGNMENT);
if (NULL == pProgramItem){
_tprintf(_T("内存分配失败!"));
return -;
}
pProgramItem->Signature = Count; //返回值为链表中以上的第1个元素,如果以前是空链表,则这里返回NULL
pFirstEntry = InterlockedPushEntrySList(pListHead,
&(pProgramItem->ItemEntry)); //也可以pProgramItem
} //删除10个元素,并显示其signature字段
for (Count = ; Count >= ;Count-=){
pListEntry = InterlockedPopEntrySList(pListHead);
if (NULL == pListEntry){
_tprintf(_T("链表是空的!"));
return -;
}
pProgramItem = (PPROGRAM_ITEM)pListEntry;
_tprintf(_T("Signature = %d\n"), pProgramItem->Signature); //释放该元素的内存
_aligned_free(pListEntry);
} //清空链表,并验证是否所有元素都己释放
pListEntry = InterlockedFlushSList(pListHead);
pFirstEntry = InterlockedPopEntrySList(pListHead);
if (pFirstEntry != NULL){
_tprintf(_T("错误:链表非空!\n"));
return -;
} _aligned_free(pListHead); _tsystem(_T("PAUSE"));
return ;
}
(4)利用InterlockedExchange实现自旋锁(spinlock)
//全局变量用来表明“共享资源”是否正在被使用
BOOl g_fResourceInUse = FALSE; void Func()
{
//等待资源的访问权——注意InterlockedExchange返回旧的值
while(InterlockedExchange(&g_fResourceInUse,TRUE)==TRUE)
Sleep(); //如果等不到锁,就休眼一下,以防止循环,浪费CPU。 //访问资源
…… //不再使用资源时
InterlockedExchange(&g_fResource,FALSE); //交出锁
}
①使用这项技术要极其小心,因为旋转锁是通过循环实现的,较耗费CPU时间,所以在while中加Sleep,可以改善这种状况,以避免浪费CPU。当然也可以用SwitchToThread代替,以便让低优先级的线程也有被调度的机会。
②特别要注意的是,在单CPU的机器上要避免使用旋转锁,因为如果这个线程一直在不停循环,对CPU浪费大,也影响了其他线程改变锁的值,造成恶性循环。
③使用旋转锁的线程优先级要相同,否则如果等待锁的线程优先级高,则使用资源的线程可能会因分配不到CPU时间而无法释放锁。所以使用这种锁的线程要通过SetProcessPrirityBoost(或SetThreadPriortyBoost)来禁用系统动态提升线程优先级
④要确保锁变量和锁所保护的数据位于不同的高速缓存行(cache line),如果在同一高速缓存行,当使用资源的CPU更改了被保护的数据,会也会其他CPU相应的高速缓存行失效,这里等待锁的CPU还要从内存中(注意:不是CPU高速缓存行)读入锁的状态,这浪费了CPU时间。
⑤旋转锁是假定被保护资源始终只会占用一小段时间。与切换到内核模式的等待相比,这种通过循环方式的等待效率更高。
8.2 高速缓存行
8.2.1 CPU、CPU高速缓存、内存的关系——见《深入理解计算机系统(第2版)P408》

(1)CPU是基于程序代码和数据在时间和空间上的局部性原理预测接下来将要用来的数据,并把这个数据(含指令)装载到高速缓存。
(2)每个CPU都有自己的高速缓存,包括指令Cache和数据Cache(其中的数据Cache一般包含多级)
(3)CPU不直接访问主存,而是通过Cache间接的访问内存。
(4)每次都从内存中读取的数据不是1个字节,而是一个Cache Line(高速缓存行,可能是32字节、64字节或128字节,取决于CPU)。
8.2.2 多处理器下的读写问题
(1)举例分析
①CPU1读取一个字节,这使得该字节及其相邻的若干字节被读到CPU1的高速缓存行
②CPU2读取同一个字节,同①一样,将那串字节的数据读到CPU2的高速缓存行
③设CPU1对内存的这个字节修改,被写进CPU1的高速缓存行,但未真正写回主存
④CPU2再次读取这个字节,由于该字节己经在CPU2的高速缓存行中,因些CPU2不需再访问内存。但CPU2将无法知道这个值已经在CPU1中得到更新的值。
(2)解决方案:
①当一个CPU修改了高速缓存行中的1个字节,其他CPU会收到通知,并使自己的缓存行作废。如CPU1修改了自己的缓存行数据,CPU2中如果相关的缓存行就作废。
②CPU2读取自己缓存行的该字时,发现己经作废,则系统会调度CPU1把新值写入主存,然后CPU2重新访问内存来填充它的高速缓存行。可见高速缓存行提高了性能,但在多CPU的机器上同样会损失性能。
(3)编程启示
①应根据数据缓存行的大小来组织应用程序的数据,使数据与缓存行边界对齐。目的是确保不同的CPU能够各自访问不同的内存地址,而这些内存地址不在同一高速缓存行中。(★获取缓存行大小:通过GetLogicalProcessInformation,传入SYSTEM_LOGICAL_PROCESSOR_INFORMATION结构体,从这个结构体的CACHE_DESCRIPTOR字段中的LineSize获得。)
②利用LineSize,通过__declspec(align(XXX))来对字段对齐加以控制。
|
糟糕的数据结构设计 |
改进后的版本 |
|
Struct CUSTINFO{ DWORD dwCustomerID;//只读,经常访问 int nBalanceDue; //读写 wchar_t szName[100]; //只读,经常访问 FILETIME ftLstOrderDate; //读写 } 【说明】避免高速缓存行可能出现问题的方法 ①使用局部变量或函数参数时,因为他们只让一个线程访问数据,就不会受其他线程影响 ②设置线程亲缘性,始终只让一个CPU访问数据。 |
#define CACHE_ALIGN 64 //64是从LineSize中获取 //强迫每个结构体放在不同的缓存行中 struct __declspec(align(CACHE_ALIGN)) CUSTINFO{ DWORD dwCustomerID; //只读,经常访问 wchar_t szName[100]; //只读,经常访问 //将以下两个字段放在不同的缓存行中 __declspec(align(CACHE_ALIGN)) int nBalanceDue; //读写 FILETIME ftLstOrderDate; //读写 } |
8.3 高级线程同步
(1)Interlocked是以原子方式修改一个值,但如果要以原子方式修改“复杂数据结构时”,需要用其他同步手段,如Critical Section、互斥锁等。
(2)旋转锁的使用应谨慎,原因是浪费CPU时间比较严重,特别是在单CPU的机器上不应使用它。
(3)当线程无法取得对资源的访问权或特殊事件尚未发生时,线程应进入不可调度的等待状态,从而避免了浪费CPU的现象。
(4)volatile限定符会告诉编译器不要对变量进行优化,而是每次读取该变量时都从内存中获取(有时为了提高效率,编译器会变量放到某个寄存器,以便快速访问。这在单线程可能没有问题,但多线程中,这个内存中的变量可能被修改,而出现寄存器的那个变量与内存变量值的不一致,volatile会强迫总是从内存中读取而不优化)。但注意以下区别
|
//变量前须加volatile volatile int g_iX = 0; DWORD WINAPI ThreadProc(…) { //g_iX可能被其他线程修改, //这里强迫每次从内存读取 int xOrg = g_iX; //以值的形式访问g_iX …… } |
//变量前无须加volatile int g_iX = 0; DWORD WINAPI ThreadProc(…) { //以下函数是以地址方式访 //问g_iX,自然就要从内存中 //中取,所以无须加volatile InterlockedIncrement(&g_iX); …… } |
(5)volatile修饰结构体时,等于结构体中所有成员都加volatile限定符。
第8章 用户模式下的线程同步(1)_Interlocked系列函数的更多相关文章
- Windows核心编程:第8章 用户模式下的线程同步
Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...
- windows核心编程---第七章 用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)
8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...
- 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)
8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...
- 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)
8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs; ...
- 《windows核心编程系列》七谈谈用户模式下的线程同步
用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...
- 【windows核心编程】 第八章 用户模式下的线程同步
Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ① 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ② 一个线程需要通知其他线程 ...
- 用户模式下的线程同步的分析(Windows核心编程)
线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...
- 《Windows核心编程》第八章——用户模式下的线程同步
下面起了两个线程,每个对一个全局变量加500次,不假思索进行回答,会认为最后这个全局变量的值会是1000,然而事实并不是这样: #include<iostream> #include &l ...
随机推荐
- Tomcat部署记事
1.导入证书到jdk里 keytool -import -alias 证书名称 -file 证书地址 -keystore 导入位置 例:keytool -import -alias co3 -file ...
- JavaScript学习笔记-表达式和语句
表达式和语句 eval( ) 只有一个参数 参数非字符串时,直接返回这个参数: 参数为字符串时,它把字符串当成JavaScript代码进行编译,编译失败则抛出语法错误,编译成功则执行代码,并返回最后一 ...
- 《Javascript高级程序设计》:创建对象
工厂模式 function createPerson(name,age, job){ var o = new Object(); o.name = name; o.age = age; o.job = ...
- 如何通过CSS3 实现响应式Web设计
如何通过CSS3 实现响应式Web设计: 分为三个步骤:(1)允许网页宽度自动调整.首先在页面头部中,我们需要加入这样一行:<meta name="viewport" con ...
- 编译安装mysql(Ubuntu10 64位)
选用较好的编译器和较好的编译器选项,这样应用可提高性能10-30%,这个对大多数程序都非常重要 Mysql的编译,不同的版本具体的配置方式是有差别的 旧版的配置形式参考 这个形式主要是使用config ...
- [Android]官网《Testing Support Library》中文翻译
以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5048524.html 翻译自 Android Develope ...
- 【Android】不使用WebView来执行Javascript脚本(Rhino)
前言 动态执行脚本能有效的降低重要功能硬编码带来的问题,尤其是依赖于第三方的应用,可以通过动态脚本+在线参数(例如友盟在线参数)再不更新应用的情况下升级功能. 声明 欢迎转载,但请保留文章原始出处:) ...
- 体验最火的敏捷——SCRUM(厦门,2014.1.4)
1.概述SCRUM是当前最火的一种敏捷开发方法,有用户故事.冲刺.燃尽图等很多很酷的玩法,有牛B的产品负责人.SCRUM Master,有超强的自组织团队.本沙龙将为您展现当前最火最酷的敏捷开发方法! ...
- Computer Network and Internet(1)
计算机网路相关的教材很少,TCP/IP,HTTP 协议非常多,很难找到一个合适的材料去学习. <计算机网络>自上而下方法是这个方面的经典之作. 1.what is internet? 1. ...
- 示例详解:UIScrollview 与 Autolayout 的那点事
前言 自从写了介绍Masonry那篇文章以后 就一直有人对UIScrollView的那个例子不是很理解 UIView *container = [UIView new]; [scrollView ad ...