第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 ...
随机推荐
- 【小贴士】zepto find元素以及ios弹出键盘可能让你很头疼
前言 在此,我不得不说移动端的兼容问题很多,并且很令人头疼,这不,这个星期又有两个让我逮着了,一个是使用zepto过程中出现的问题,一个是ios虚拟键盘的问题 我这里做一次记录,以免以后忘了,同时希望 ...
- VS2010在64位系统中连接64位Oracle出现的问题和解决方法
C#使用System.Data.OracleClient连接Oracle数据库.我的是window7/64位系统,装了一个64位的oralce 11G r2 客户端是64位的 用VS10调试错误信息如 ...
- JavaScript中的各种变量提升(Hoisting)
首先纠正下,文章标题里的 “变量提升” 名词是随大流叫法,“变量提升” 改为 “标识符提升” 更准确.因为变量一般指使用 var 声明的标识符,JS 里使用 function 声明的标识符也存在提升( ...
- gif动图快速制作方法(附工具)
现在写博客或是wiki的过程中,会经常引用到图片,特别是客户端经常与页面相关所以截图不可避.但是越来越多的效果仅仅一张图片是无法清楚的描述.并且博客或是wiki也是支持gif图的.gif图的制作方法有 ...
- 【代码笔记】iOS-判断字符串是否为空
一,代码. - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. ...
- fir终端打包,亲测可用
1.注册fir.拿到token 2.安装 fir-cli 使用 Ruby 构建, 无需编译, 只要安装相应 gem 即可. $ ruby -v # > 1.9.3 $ gem install f ...
- iOS开发之UISearchBar初探
iOS开发之UISearchBar初探 UISearchBar也是iOS开发常用控件之一,点进去看看里面的属性barStyle.text.placeholder等等.但是这些属性显然不足矣满足我们的开 ...
- github邮箱验证技巧
申请的github账号,绑定邮箱之后才能创建库,而反复几次的发送邮件均为收到验证邮件,猜测有两个原因: 1.腾讯邮件服务器屏蔽了github的来信 (腾讯不会这么狭隘的,×) 2.自己邮箱的域名黑名单 ...
- [oracle]数据库语言分类
一般来说,数据库语言可以分成以下5大类: 1.数据定义语言DDL(Data Definition Language),用于改变数据库结构,包括创建.修改和删除数据库对象.包括create(创建).al ...
- Highcharts使用简例 + 异步动态读取数据
第一部分:在head之间加载两个JS库. <script src="html/js/jquery.js"></script> <script src= ...