文章目录

  • 1. 原子访问:Interlocked系列函数
  • 2. 高速缓存行
  • 3. 高级线程同步
  • 4. 关键段
  • 5. Slim读/写锁
  • 6. 一些有用的窍门和技巧

当所有的线程都能够独自运行而不需要相互通信的时候,Microsoft Windows将进入最佳运行状态。但是,很少有线程能够总是独自运行。通常创建线程是为了处理某些任务,当任务完成的时候,另一个线程可能想要得到通知。

系统中所有的线程必须访问系统资源,比如堆、串口、文件、窗口以及无数其他资源。如果一个线程独占了对某个资源的访问,那么其他线程就无法完成它们的工作。另一方面,我们也不能让任何线程在任何时刻都能访问任何资源。设想有一个线程正在写入一块内存,而同时另一个线程正在从同一块内存中读取数据。这就好比是一个人在另一个人读书的时候修改书中的文字一样,书中的内容将变得乱七八糟,毫无用处。

在以下两种基本情况下,线程之间需要相互通信:

  • 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性。
  • 一个线程需要通知其他线程某项任务已经完成。

线程同步包括许多方面,我们会在下面的几章中进行讨论。好消息是Microsoft Windows提供了许多基础设施,可以让线程同步变得容易。但坏消息是我们很难预见一堆线程在任一时刻打算做什么。我们大脑的工作方式不是异步的,我们习惯一次一步地按次序考虑间题,但这不是多线程环境的运作方式。

我最早开始使用多线程大概是在192年。一开始,我在编写程序时犯了许多错误,甚至还出版了一些书和杂志文章,其中不乏与线程同步有关的缺陷。现在,我已经比当时熟练得多,虽然还谈不上完美,但我相信本书中的一切都不存在缺陷。想要熟练掌握线程同步,唯一途径就是实际使用。在下面几章中,我们会解释系统的运作方式,并展示如何以正确的方式在线程间进行同步。现在让我们面对现实,在积累经验的过程中我们会犯这样那样的错误,但这并没有什么大不了的。

1. 原子访问:Interlocked系列函数

线程同步的一大部分与原子访问(atomic access)有关。所谓原子访问,指的是一个线程在访问某个资源的同时能够保证没有其他线程会在同一时刻访问同一资源。现在让我们来看一个简单的例子:

// 定义全局变量
long g_x = 0;DWORD WINAPI ThreadFunc1(PVOID pvParam){g_x++;return(0);
}DWORD WINAPI ThreadFunc2(PVOID pvParam){g_x++;return(0);
}

代码中声明了一个全局变量gx并将它初始化为0。现在假设我们创建了两个线程,一个线程执行ThreadFuncl,另一个线程执行ThreadFunc2。这两个函数中的代码完全相同:
它们都把全局变量gx加1。因此当两个线程都停止运行的时候,我们可能认为gx的值会是2。但真的是这样吗?答案是有可能。根据代码的编写方式,我们无法确切地知道gx最终会等于几,下面就是原因。假设编译器在编译将gx递增的那行代码时,生成了下面的汇编代码:

我们需要有一种方法能够保证对一个值的递增操作是原子操作一—也就是说,不会被打断。Interlocked 系列函数提供了我们需要的解决方案。虽然这些Interlocked函数非常有用,也很容易理解,但大多数软件开发人员对它们心存畏惧,并没有充分地利用它们。所有这些函数会以原子方式来操控一个值。让我们来看看InterlockedExchangeAdd 以及它用来对LONGLONG类型进行操控的兄弟函数InterlockedExchangeAdd64:

LONG InterlockedExchangeAdd(LONG volatile *Addend,LONG          Value
);LONG64 InterlockedExchangeAdd64(LONG64 volatile *Addend,LONG64          Value
);

还有什么方法能比这更简单吗?只要调用这个函数,传一个长整型变量的地址和另一个增量值,函数就会保证递增操作是以原子方式进行的。因此我们可以把前面的代码改写成下面的代码:

// 定义全局变量
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);
}

经过这个微小的改动,对息x的递增会以原子方式进行,我们也因此能够保证要x最终的值将等于2。注意,如果只想以原子方式给一个值加1的话,也可以使用Interlockedlncrement函数。现在是不是已经感觉好些了?要注意的是,所有线程都应该调用这些函数来修改共享变量的值,任何一个线程都不应该使用简单的C+语句来修改共享变量:

2. 高速缓存行

如果想为装配有多处理器的机器构建高性能应用程序,那么应该注意高速缓存行。当CPU从内存中读取一个字节的时候,它并不只是从内存中取回一个字节,而是取回一个高速缓存行。高速缓存行可能包含32字节(老式CPU),64字节,甚至是128字节(取决于CPU),它们始终都对齐到32字节边界,64字节边界,或128字节边界。高速缓存行存在的目的是为了提高性能。一般来说,应用程序会对一组相邻的字节进行操作。如果所有字节都在高速缓存中,那么CPU就不必访问内存总线,后者耗费的时间比前者耗费的时间要多得多。

但是,在多处理器环境中,高速缓存线使得对内存的更新变得更加困难。我们可以从下面的例子中体会到这一点。

(1)CPU1读取一个字节,这使得该字节以及与它相邻的字节被读到CPU1的高速缓存行中。
(2)CPU2读取同一个字节,这使得该字节被读到CPU2的高速缓存行中。
(3)CPU1对内存中的这个字节进行修改,这使得该字节被写入到CPU1的高速缓存行中。
但这一信息还没有写回到内存。
(4)CPU2再次读取同一个字节。由于该字节已经在CPU2的高速缓存行中,因此CPU2不需要再访问内存。但CPU2将无法看到该字节在内存中新的值。

这种情形非常糟糕。当然,CPU芯片的设计者非常清楚这个问题,并做了专门的设计来对它进行处理。明确地说,当一个CPU修改了高速缓存行中的一个字节时,机器中的其他CPU会收到通知,并使自己的高速缓存行作废。因此在刚才的情形中,当CPU1修改该字节的值时,CPU2的高速缓存就作废了。在第4步中,CPU1必须将它的高速缓存写回到内存中,CPU2必须重新访问内存来填满它的高速缓存行。我们可以看到,虽然高速缓存行能够提高性能,但在多处理器的机器上它们同样能够损伤性能。

最好是始终只让一个线程访问数据(函数参数和局部变量是确保这一点的最简单方式),或者始终只让一个CPU访问数据(使用线程关系,即thread affinity)。只要能做到其中任何一条,就可以完全避免高速缓存行的问题了。

3. 高级线程同步

如果只需要以原子方式修改一个值,那么Interlocked系列函数非常好用,我们当然应该优先使用它们。但大多数实际的编程问题需要处理的数据结构往往要比一个简单的32位值或64位值复杂得多。为了能够以“原子”方式来访问复杂数据结构,我们必须超越Interlocked系列函数,转而使用Windows提供的一些其他特性。

前面一节强调了在配备单处理器的机器上不应该使用旋转锁,即使在配备多处理器的机器上,在使用旋转锁的时候也应该谨慎。原因很简单,浪费CPU时间是件非常糟糕的事情。因此,我们需要一种机制,它既能让线程等待共享资源的访问权,又不会浪费CPU时间。当线程想要访问一个共享资源或者想要得到一些“特殊事件”的通知时,线程必须调用操作系统的一个函数,并将线程正在等待的东西作为参数传入。如果操作系统检测到资源已经可供使用了,或者特殊事件已经发生了,那么这个函数会立即返回,这样线程将仍然保持可调度状态。(线程可能并不会立即运行,它是可调度的,系统会根据前一章中描述的规则来给它分配CPU时间。)

如果无法取得对资源的访问权,或者特殊事件尚未发生,那么系统会将线程切换到等待状态,使线程变得不可调度,从而避免了让线程浪费CPU时间。当线程在等待的时候,系统会充当它的代理。系统会记住线程想要访问什么资源,当资源可供使用的时候,它会自动将线程唤醒——线程的执行与特殊事件是同步的。

实际情况是,大多数线程在大部分情况下都处于等待状态。当系统检测到所有线程都已经在等待状态中度过了好几分钟的时候,系统的电源管理器将会介入。

需要避免使用的一种方法:

如果没有同步对象,如果操作系统不能对特殊事件进行监测,那么线程将不得不使用下面介绍的技术来在自己和特殊事件之间进行同步。但是,由于操作系统内建了对线程同步的支持,因此我们在任何时候都不应该使用这种方法。

在这种方法中,两个线程共享一个变量,其中一个线程不断地读取变量的值,直到另一个线程完成它的任务为止。

4. 关键段

关键段(critical section)是一小段代码,它在执行之前需要独占对一些共享资源的访问权。这种方式可以让多行代码以“原子方式”来对资源进行操控。这里的原子方式,指的是代码知道除了当前线程之外,没有其他任何线程会同时访问该资源。当然,系统仍然可以暂停当前线程去调度其他线程。但是,在当前线程离开关键段之前,系统是不会去调度任何想要访问同一资源的其他线程的。

5. Slim读/写锁

与关键段相比,SRWLock缺乏下面两个特性:

  • 不存在TryEnter(Shared/Exclusive)SRWLock之类的函数:如果锁已经被占用,那么调用AcquireSRWLock(Shared/Exclusive)会阻塞调用线程。
  • 不能递归地获得SRWLOCK。也就是说,一个线程不能为了多次写入资源而多次锁定资源,然后再多次调用ReleaseSRWLock*来释放对资源的锁定。

6. 一些有用的窍门和技巧

当我们在使用锁的时候,比如关键段或读取者/写入者锁,应该养成一些良好的习惯并避免一些不太好的做法。下面几个窍门和技巧会对锁的使用有所帮助。这些技巧也同样适用于内核同步对象(会在下一章讨论)。

1. 以原子方式操作一组对象时使用一个锁:

一种常见的情况是多个对象聚在一起会构成一个单独的“逻辑”资源。例如,每当我们向一个集合中添加元素的时候,可能同时需要对另一个计数器进行更新。为此,无论我们需要对这个逻辑资源进行读操作还是写操作,都应该只使用一个锁。

应用程序中的每个逻辑资源都应该有自己的锁,用来对逻辑资源的部分和整体的访问进行同步。我们不应该为所有的逻辑资源都创建单独的锁,这是因为如果多个线程访问的是不同的逻辑资源,那么这样做会降低可伸缩性:任一时刻系统只允许一个线程执行。

2. 同时访问多个逻辑资源:

有时我们需要同时访问两个(或更多个)逻辑资源。例如,应用程序可能需要锁定一个资源来取出一个元素,同时锁定另一个资源来把元素加入其中。如果每个资源都有自己的锁,那么我们必须使用所有的锁才能以原子方式完成这个操作。

3.不要长时间占用锁:

如果一个锁被长时间占用,那么其他线程可能会进入等待状态,这会影响到应用程序的性能。我们可以用下面这个技巧来把花在关键段中的时间降至最低。下面的代码会在WM_SOMEMSG消息被发送到另一个窗口之前阻止其他线程修改gs的值:

windows核心编程之用户模式下的线程同步相关推荐

  1. 用户模式下的线程同步

    在以下两种基本情况下,线程之间需要相互通信 1.需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 2.一个线程需要通知其他线程某项任务已经完成. 原子访问相关的内容就直接略过了,因为感觉实 ...

  2. Windows用户模式下的线程同步

    Interlocked系列函数 原子访问:线程在访问某个资源的时候能保证没有其他线程会在同一时刻访问同一资源 函数名 功能 InterlockedExchangeAdd InterlockedExch ...

  3. 《Windows核心编程》学习笔记(10)– 同步设备I/O与异步设备I/O

    1.打开和关闭设备 Windows的优势之一是它所支持的设备数量.就我们的讨论而言,我们把设备定义为能够与之进行通信的任何东西.表1列出了一些设备及其常见用途. 表1:各种设备及其常见用途 设备 常见 ...

  4. Chapter09-内核模式下的线程同步之事件内核对象

    有两种事件内核对象:自动事件和手动事件.当手动事件被触发时,所以该事件的等待线程都编程可调度状态:而自动事件被触发时,只有个一个等待该事件线程变成可调度状态. 下面再逐个讲解Event的相关函数: a ...

  5. [笔记]Windows核心编程《二十》DLL的高级操作技术

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  6. [笔记]Windows核心编程《十六》线程栈

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  7. [笔记]Windows核心编程《十九》DLL基础

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  8. [笔记]Windows核心编程《十三》windows内存体系结构

    系列文章目录 [笔记]Windows核心编程<一>错误处理.字符编码 [笔记]Windows核心编程<二>内核对象 [笔记]Windows核心编程<三>进程 [笔记 ...

  9. C++Windows核心编程读书笔记(转)

    http://www.makaidong.com/(马开东博客) 这篇笔记是我在读<windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的 ...

  10. [C++]《Windows核心编程》读书笔记

    这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入 ...

最新文章

  1. VGG - Very Deep Convolutional Networks for Large-Scale Image Recognition
  2. pom配置之:distributionManagementsnapshot快照库和release发布库
  3. c6011取消对null指针的引用_C++| 函数的指针参数如何传递内存?
  4. 温泉季节到了,设计师需要的SPA插画,完美体现那一池的温暖!
  5. 52 -算法 -数据结构类 Leetcode26 删除有序数组中的重复项
  6. sublime text3 之 ctags
  7. JDK6和JDK7中的substring()方法
  8. 华为中兴FPGA面试题总结
  9. 三星 android驱动安装失败,三星安卓手机usb驱动安装教程
  10. JAVA 身份证号码的验证
  11. 测试显卡矿卡用什么软件,3分钟看懂:AMD二手矿卡简明鉴别、检测教程,从此脱坑不求人...
  12. 网络舆情监测TOOM
  13. 【PI控制】位置式PI的拉普拉斯变化和离散化(在开关电源的应用)
  14. 原生js和jquery 获取文档高度
  15. 阿里云EMAS移动测试|快速掌握移动端兼容性测试技巧
  16. 第十一章 初窥天机之数据类型为我所用
  17. bzoj 3161: 孤舟蓑笠翁 bfs
  18. 【开发工具】Linux环境下JDK安装(无错完整)
  19. PyGmae:有限状态机实践(十三)
  20. 战地2服务器2地图修改,【战地2怎么将地图改为32人】如何修改地图_战地2修改地图教程_游戏城...

热门文章

  1. 如何将英文句子分词(拆分单词), 并判断分词是否为英文单词
  2. TMUX Cheat Table:和那些妖艳贱货不一样的 TMUX 教程
  3. 梦幻西游战斗中服务器维护,梦幻西游10月22日维护公告 连续战斗自动问题修复...
  4. Vue 中获取 package.json 信息
  5. TensorFlow的Dataset的padded_batch使用
  6. 剑英陪你玩转图形学(五)focus
  7. unity LineRender结合多点触摸 实现拖拽 重复画线
  8. P2184 贪婪大陆(线段树)
  9. C++计算圆柱体的表面积
  10. dist文件夹、src文件夹、dest文件夹是什么意思?