本章主要内容
- 线程间划分数据的技术
- 影响并发代码性能的因素
- 性能因素是如何影响数据结构的设计
- 多线程代码中的异常安全
- 可扩展性
- 并行算法的实现
前面主要介绍了并发的数据结构,现在从高层(但也是基本的)考虑,如何使用线程,哪些代码应该在哪些线程上执行;以及,这将如何影响代码的清晰度,并从底层细节上了解,如何构建共享数据来优化性能。
一、线程间划分工作的技术
使用线程,可以使用全能的线程,用调度器调度;或者使用“专业”线程只专注完成一件事,每个线程都有分工,或者二者混合。
1、在线程处理前对数据进行划分。
最简单的并行算法,就是并行化的 std::for_each :单位数据分配一个线程,各个线程单独工作不会沟通,在主线程合并。
2、递归划分
(并行算法博客里面有)快速排序模型
3、任务类型划分
分离关注:QT程序将图像处理交给图像处理线程,防止界面卡住;主控线程等待用户操作和线程返回。
划分任务序列:当任务会应用到相同操作序列,去处理独立的数据项时,就可以使用流水线(pipeline)系统进行并发。使用这种方式划分工作,可以为流水线中的每一阶段操作创建一个独立线程。当一个操作完成,数据元素会放在队列中,以供下一阶段的线程提取使用。
这种流水线看起来不能保证每个线程时刻都有工作,在开头结尾会有等待。但是一旦流动起来,对瓶颈的数据段加大优化,实现快速移动。举例:为了让视频能够播放,你至少要保证25帧每秒的解码速度。同样的,这些图像需要有均匀的间隔,才会给观众留有连续播放的感觉;一个应用可以在1秒解码100帧,不过在解完就需要暂停1s的时候,这个应用就没有意义了。另一方面,观众能接受在视频开始播放的时候有一定的延迟。这种情况,并行使用流水线就能得到稳定的解码率。
二、影响并发的因素
1、处理器数量
一个单核16芯的处理器和四核双芯或十六核单芯的处理器相同:在任何系统上,都能运行16个并发线程。当线程数量少于16个时,会有处理器处于空闲状态(除非系统同时需要运行其他应用,不过我们暂时忽略这种可能性)。另一方面,当多于16个线程在运行的时候(都没有阻塞或等待),应用将会浪费处理器的运算时间在线程间进行切换,这种情况发生时,我们称其为超额认购。
std::thread::hardware_concurrency() 可以获取处理器数目,但是他不会考虑正在运行的其他线程,很容易超额认购。std::async() 就能避免这个问题,因为标准库会对所有的调用进行适当的安排。同样,谨慎的使用线程池也可以避免这个问题。
2、数据争用与乒乓缓存
3、超额认购导致的频繁任务切换
4、伪共享
处理器缓存通常不会用来处理在单个存储位置,但其会用来处理称为缓存行(cache lines)的内存块。比如:通常int类型的大小要小于一个缓存行,同一个缓存行中可以存储多个数据项。每当线程访问0号数据项,并对其值进行更新时,缓存行的所有权就需要转移给执行该线程的处理器。而此时虽然缓存行是共享的(即使没有数据存在),但是其他处理器没有控制权,无法访问到数据。因此使用伪共享来称呼这种方式。
三、提高多线程性能的数据结构
尝试调整数据在线程间的分布,就能让同一线程中的数据紧密联系在一起。
尝试减少线程上所需的数据量。
尝试让不同线程访问不同的存储位置,以避免伪共享。