第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 ...
随机推荐
- go语言 新手学习笔记 go基础教程
目前这方面的资料相对较少,自己手动整理汇集. 第一章:安装 第一节:下载go语言 第二节:windows 安装 go语言 第三节: 第二章:基本语法 第一节:类型 .
- C#关于word文档的书签替换操作
public void Get_Word(string gjbh) { try { DataSet ds = OperaterBase.GetDsBySql("select diffTabl ...
- Could not load file or assembly 'Microsoft.SqlServer.Management.Sdk.Sfc, Version=11.0.0.0 系统找不到指定的文件。
环境: web服务器: ip:192.168.1.32 ,安装有 Visual Studio Premium 2013 操作系统: Microsoft Server 2008 r2+sp1 数据库服 ...
- SharePoint Online 创建门户网站系列之母版页
前 言 虽然SharePoint中母版页看起来只是头部Banner和底部版权信息这两个部分,但是实质在SharePoint Online中的页面模型中占有重要地位,而且SPO对母版页有着完善的签入签出 ...
- [其他]Ubuntu安装genymotion后unable to load VirtualBox engine
问题: Ubuntu安装genymotion后unable to load VirtualBox engine 解决办法: 如果没有安装VirtualBox,要先安装VirtualBox. 安装Vir ...
- android 数据存储Ⅰ
本章讲述在Android开发中,简单的数据存储.涉及知识主要是SharedPreferences,及多页面切换ViewPager. 1.功能需求 做一个小应用.启动的时候有左右引导图.只有第一次启动时 ...
- vs出现“已经在解决方案中打开了具有该名称的项目”问题的解决方案
经过本人测试,这种问题一般出现在装了svn的项目. 其实删除了删除sln和csproj文件中的SVN配置信息就行了 需要删除的信息 sln文件中: GlobalSection(SubversionSc ...
- Bootstrap弹出框(modal)垂直居中
最近在做一个eit项目,由于此项目里面一些框架要遵循nttdata的一些规则,故用到了Bootstrap这个东东,第一次碰到这个东东,有很大抵触,觉得不好,但用起来我觉得和别的弹出框真没什么两样.废话 ...
- mysql多实例的配置和管理
原文地址:mysql多实例的配置和管理 作者:飞鸿无痕 多实例mysql的安装和管理 mysql的多实例有两种方式可以实现,两种方式各有利弊.第一种是使用多个配置文件启动不同的进程来实现多实例,这种方 ...
- 纯css实现照片墙3D效果
每张照片都有美丽的故事.美好的回忆.家居中的照片墙则帮你展现出这些承载着家庭重要记忆的照片,除了用画框装饰照片挂在墙上外,照片墙还可以演变为手绘照片墙.也经常在网上看到一些关于照片墙的特效案例,决定自 ...