多线程
CPU时钟频率限制于物理硬件因素。在时钟频率的限制下,提高CPU密集型程序吞吐量的方式是,同时进行多个任务。任务并行有三种方式:
- 使用多个CPU或多核CPU;
- 利用现在CPU的乱序能力;
- 利用现代CPU的向量操作;
为了利用CPU的多核能力,需要把问题拆分为多个线程。有两个准则:功能分解和数据分解。功能分析意味着不同的线程做不同种类的任务。
然而,在许多场景,会有一个任务消耗了大多数资源。在这种情况下,我们需要把数据分割为多个块来利用CPU的多核。每个线程处理它自己的数据块。这就是数据分解。
在决定并行工作是否有益时,区分粗粒度并行和细粒度并行是重要的。粗粒度并行:一个长操作序列能够独立于其他正在并行执行的任务并行地运行。细粒度并行:一个任务可以分解为许多小的子任务,但特定的子任务需要与其他子任务协作,否则特定子任务不能长时间执行。
多线程在粗粒度并行下比细粒度并行下工作的刚好,因为线程间通信和同步是慢的。如果粒度太细,那么把任务分成多线程并不有益。乱序执行和向量操作(后续探讨)对于运用细粒度并行更有用。
对于数据分解,相同优先级的线程数不应该多于处理器的核数。可用的逻辑处理器的数目可以通过系统调用获得(例如,Windows中的GetProcessAffinityMask
)。
有几种方式在多CPU核之间分配负载:
- 定义多个线程并分配等量的工作到各个线程。此方法适用于所有编译器。
- 使用自动并行功能。Gnu和Intel编译器能自动侦测代码中的并行的机会并拆分到多个线程。但编译器可能不能够找到数据的优化分解方法。
- 使用OpenMP指令。OpenMP是指定C++和Fortran中的并行执行的标准。这个指令被大多数编译器支持。可参考www.openmp.org和编译器手册。
- 使用
std::thread
。功能比OpenMP少,但是标准跨平台的。 - 使用内部支持多线程的函数库,例如 Intel Math Kernel Library。
多个CPU核或逻辑处理器通常共享相同的cache,至少是最后一级缓存;有些情形甚至共享level-1 cache。共享cache的好处是:线程间通信更快,线程能够共享相同的代码和只读数据。坏处是:如果不同线程使用不同的内存区域,cache很快会耗尽;如果不同线程写相同的内存区域,会发生cache冲突。
只读数据可以在线程间共享;被修改的数据应当在线程间互相分离。如果不同的线程写相同的cache区,会让彼此的cache失效,引起大的延迟。创建线程自身数据的最简单的方法是:在线程函数里定义数据,这些数据就会存储在线程自己的栈上。另外一个替代方案是,定义结构体霍磊来容纳线程数据,并为每个线程创建一个实例。这个结构体或类应该对齐到cache区,以避免多线程写相同的cache区。
有多种线程间通信和同步方法,例如:信号量、互斥量和消息系统。所有这些方法都耗时的。因此,数据和资源应该好好组织以便使得线程通信量最小化。
在一个单核处理器上运行多线程程序没有好处。但是,把耗时的计算任务分离到一个比用户接口低优先级的线程仍然是一个好主意。把文件访问和网络访问分离到单独的线程也是有用的。
线程同步
很多微处理器都可以在一个核上运行两个线程。例如,一个四核处理器可以同时运行八个线程。这个处理器就有四个物理处理器,但有八个逻辑处理器。
在共享的资源是性能瓶颈时,使用同步多线程没有好处。相反,由于cache替换和其他资源竞争,每个线程可能运行速度可能不到预期的一半。但是,如果cache是失效、错误分支预测或长依赖链耗费了很多时间,那么每个线程可能运行速度快于单线程的一半。这时,利用多线程技术是有益的,但是性能并不会翻倍。
对于特定的应用程序,对是否采用多线程技术进行性能测试是有必要的。如果使用线程同步是不好的,那么有必要通过特定的操作系统函数(例如,Windows中的GetLogicalProcessorInformation
)获取同步多线程技术的支持信息。如果操作系统支持多线程同步,你可以通过只使用偶数编号的逻辑处理器来避免线程同步。以前的操作系统缺乏必要的函数来区分物理处理器数目和逻辑处理器数目。
对于在同一个核上运行的线程,没有办法通用的方法让处理器给一个线程比另一个线程更高的优先级。因此,经常发生一个低优先级的线程从运行在同一个核上的高优先级线程窃取资源的事情。避免两个优先级差别很大的线程在同一个核上运行是操作系统的责任。不幸的是,现在的操作系统并不能很好的处理这个问题。
欢迎交流