C++性能优化笔记-9-多线程

多线程


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)获取同步多线程技术的支持信息。如果操作系统支持多线程同步,你可以通过只使用偶数编号的逻辑处理器来避免线程同步。以前的操作系统缺乏必要的函数来区分物理处理器数目和逻辑处理器数目。

对于在同一个核上运行的线程,没有办法通用的方法让处理器给一个线程比另一个线程更高的优先级。因此,经常发生一个低优先级的线程从运行在同一个核上的高优先级线程窃取资源的事情。避免两个优先级差别很大的线程在同一个核上运行是操作系统的责任。不幸的是,现在的操作系统并不能很好的处理这个问题。

欢迎交流
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
原子类型和原子操作是并发编程中的重要概念。在多线程编程中,当多个线程同时操作同一个共享变量时,可能会出现数据竞争的问题,导致程序的结果不可预测。为了解决这个问题,C11引入了原子类型和原子操作。 原子类型是一种特殊的数据类型,它的操作都是原子的,即不会被其他线程的操作干扰。C11提供了几种原子类型,包括原子整型、原子指针和原子布尔型等。原子类型可以在多线程环境下安全地进行读写操作,保证数据的一致性。 原子操作是对原子类型进行的操作,包括赋值、递增、递减等。这些操作都是原子的,不会被其他线程的操作干扰。原子操作通过一些特殊的语法和函数来实现,如原子操作的语法是“Atomics”开头的函数。例如,使用原子操作可以通过atomic_store函数将一个值存储到原子类型的变量中,保证线程安全。 使用原子类型和原子操作可以简化并发编程的复杂度,避免数据竞争。原子类型和原子操作提供了一种高效的并发编程模型,在多线程编程中具有重要的应用价值。 在使用原子类型和原子操作时,需要注意一些问题。首先,原子操作虽然保证了操作的原子性,但并不能完全解决所有的并发问题。其次,原子操作的性能可能不如普通操作,因为原子操作需要保证线程安全,可能需要加锁等额外开销。 总之,原子类型和原子操作是C11提供的一种并发编程的解决方案。通过使用原子类型和原子操作,可以有效解决多线程编程中的数据竞争问题,提高程序的并发性和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值