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系列函数的更多相关文章

  1. Windows核心编程:第8章 用户模式下的线程同步

    Github https://github.com/gongluck/Windows-Core-Program.git //第8章 用户模式下的线程同步.cpp: 定义应用程序的入口点. // #in ...

  2. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  3. 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)

    8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...

  4. 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)

    8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...

  5. 第8章 用户模式下的线程同步(2)_临界区(CRITICAL_SECTION)

    8.4 关键段(临界区)——内部也是使用Interlocked函数来实现的! 8.4.1 关键段的细节 (1)CRITICAL_SECTION的使用方法 ①CRITICAL_SECTION cs;   ...

  6. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  7. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  8. 用户模式下的线程同步的分析(Windows核心编程)

    线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...

  9. 《Windows核心编程》第八章——用户模式下的线程同步

    下面起了两个线程,每个对一个全局变量加500次,不假思索进行回答,会认为最后这个全局变量的值会是1000,然而事实并不是这样: #include<iostream> #include &l ...

随机推荐

  1. 【javascript激增的思考02】模块化与MVC

    前言 之前我们遇到了这么一个项目,也就是我们昨天提到的,有很多的小窗口的,昨天说的太抽象了,今天我们再来理一理什么是小窗口(后面点说下),当时由于js有一点复杂,我自己也装B跟风用了一下传说中MVC! ...

  2. git怎么创建本地版本仓库

    git怎么创建本地版本仓库 安装git我就不用说了吧!下载地址:https://github.com/msysgit/msysgit/releases/download/Git-1.9.4-previ ...

  3. 使用PDFCreate 和 Powershell 自动保存网页为PDF

    先安装PDF Creator. http://rj.baidu.com/soft/detail/10500.html?ald 把他设置为默认打印机. 在IE中设置打印页面的边距,页眉页脚等. Powe ...

  4. 【Leafletjs】1.创建一个地图

    code: <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <l ...

  5. html ul li的学习

    DIV+CSS里,我们用得最多的就是ul li来显示数据,如新闻按钮等. <div id="menu"> <ul> <li><a href ...

  6. 当SD卡拔出时,返回首页,栈中的activity都要清除,只留下首页的activity

    目标:当SD卡拔出时,返回首页,栈中的activity都要清楚,只留下首页的activity 我在清单中注册了一个静态广播: <receiver android:name="com.p ...

  7. 【C语言】C语言数据类型

    目录: [数据类型图] [基本数据类型]   · 整型   · 实型   · 字符型   · 布尔类型 1.数据类型图 2.基本数据类型 · 整型 用于准确表示整数,根据表示范围的不同分为三种:短整型 ...

  8. tomcat WEB-INF中的结构

    tomcat中 WEB-INF中结构包含3个东西:web.xml,classes文件夹,lib文件夹 web.xml用来配置web中服务调用的uri和对应服务指定的是哪个class文件 classes ...

  9. iOS 跳转到App Store下载或评论

    //跳转到app在AppStore页面 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString string ...

  10. R语言中的循环函数(Grouping Function)

    R语言中有几个常用的函数,可以按组对数据进行处理,apply, lapply, sapply, tapply, mapply,等.这几个函数功能有些类似,下面介绍下这几个函数的用法. Apply 这是 ...