c++ 多线程编程学习(一)

目录

  • 并发
  • 并发使用场景
  • C++多线程
    参考书籍:C++ Concurrency in Action

并发

计算机领域的并发指的是在单个系统里同时执行多个独立的任务,而非顺序的进行一些活动。
单核处理器:这种机器只能在某一时刻执行一个任务,不过它可以每秒进行多次任务切换。通过“这个任务做一会,再切换到别的任务,再做一会儿”的方式,让任务看起来是并行执行的。这种方式称为任务切换。如今,我们仍然将这样的系统称为并发:因为任务切换得太快,以至于无法感觉到任务在何时会被暂时挂起,而切换到另一个任务。任务切换会给用户和应用程序造成一种“并发的假象”。
多核处理器:无论拥有几个处理器,这些机器都能够真正的并行多个任务。我们称其为硬件并发(hardware concurrency)”。
在这里插入图片描述
上图显示了一个计算机处理恰好两个任务时的理想情景,每个任务被分为10个相等大小的块。在一个双核机器(具有两个处理核心)上,每个任务可以在各自的处理核心上执行。在单核机器上做任务切换时,每个任务的块交织进行。但它们中间有一小段分隔(图中所示灰色分隔条的厚度大于双核机器的分隔条);为了实现交织进行,系统每次从一个任务切换到另一个时都需要切换一次上下文(context switch),任务切换也有时间开销。进行上下文的切换时,操作系统必须为当前运行的任务保存CPU的状态和指令指针,并计算出要切换到哪个任务,并为即将切换到的任务重新加载处理器状态。然后,CPU可能要将新任务的指令和数据的内存载入到缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟。

并发的途径

多进程并发

使用并发的第一种方法,是将应用程序分为多个独立的进程,它们在同一时刻运行,就像同时进行网页浏览和文字处理一样。如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、文件、管道等等)。不过,这种进程之间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以避免一个进程去修改另一个进程的数据。还有一个缺点是,运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程,等等。
在这里插入图片描述
进程切换:

  1. 保存处理器的上下文,包括程序计数器和其它寄存器
  2. 用新状态和其它相关信息更新正在运行进程的PCB
  3. 把原来的进程移至合适的队列-就绪、阻塞
  4. 选择另一个要执行的进程
  5. 更新被选中进程的PCB
  6. 从被选中进程中重装入CPU 上下文

多线程并发

并发的另一个途径,在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立运行,且线程可以在不同的指令序列中运行。但是,进程中的所有线程都共享地址空间,并且所有线程访问到大部分数据———全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然,进程之间通常共享内存,但是这种共享通常是难以建立和管理的。因为,同一数据的内存地址在不同的进程中是不相同。下图展示了一个进程中的两个线程通过共享内存进行通信。
在这里插入图片描述
地址空间共享,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多线程相关的开销远远小于使用多个进程。不过,共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保每个线程所访问到的数据是一致的
线程切换:
一个处理器都只会执行一条线程中的指令,为了让线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,进行线程上下文切换时记录程序计数器、CPU寄存器状态(记录有挂起变量的值)等数据即可。

进程和线程的切换比较:

(1)进程切换方式:切换虚拟地址空间,切换内核栈和硬件上下文
(2)线程切换方式:切换内核栈和硬件上下文
(3)虚拟地址空间的切换:切换页表,以使用新的地址空间,虚拟内存和物理内存会进行一一对应数据存放,页表(虚拟内存)可以将虚拟地址转换为物理内存地址,从而能够通过页表查找到虚拟地址空间的中的某一数据在物理内存的具体位置。
(4)页表切换的开销:通常使用TLB Cache来进行缓存常用的地址映射,用来加速页表查找,当进程切换后,页表页要进行切换,页表切换后TLB就会失效,Cache失效导致查找命中率降低,也就是虚拟地址转换为物理地址就会变慢,程序中其他进程的执行就变慢,表现出来程序运行会慢。

并发使用场景

使用并发的主要原因有两个:关注点分离(SOC)和性能。

关注点分离

通过将相关的代码与无关的代码分离,可以使程序更容易理解和测试,从而减少出错的可能性。即使一些功能区域中的操作需要在同一时刻发生的情况下,依旧可以使用并发分离不同的功能区域。
考虑一个有用户界面的处理密集型应用——DVD播放程序。这样的应用程序,应具备这两种功能:一,要从光盘中读出数据,对图像和声音进行解码,之后把解码出的信号输出至视频和音频硬件,从而实现DVD的无误播放;二,还需要接受来自用户的输入,当用户单击“暂停”、“返回菜单”或“退出”按键的时候执行对应的操作。当应用是单个线程时,应用需要在回放期间定期检查用户的输入,这就需要把“DVD播放”代码和“用户界面”代码放在一起,以便调用。如果使用多线程方式来分隔这些关注点,“用户界面”代码和“DVD播放”代码就不再需要放在一起:一个线程可以处理“用户界面”事件,另一个进行“DVD播放”。它们之间会有交互(用户点击“暂停”),不过任务间需要人为的进行关联。

性能

两种方式利用并发提高性能:第一,将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)。虽然这听起来很直观,但它是一个相当复杂的过程,因为在各个部分之间可能存在着依赖。区别可能是在过程方面——一个线程执行算法的一部分,而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在不同的数据部分上执行相同的操作(第二种方式)。后一种方法被称为数据并行(data parallelism)。

什么时候不使用并发

基本上,不使用并发的唯一原因就是,收益比不上成本。
因为操作系统需要分配内核相关资源和堆栈空间,所以在启动线程时存在固有的开销,然后才能把新线程加入调度器中,所有这一切都需要时间。如果在线程上的任务完成得很快,那么任务实际执行的时间要比启动线程的时间小很多,这就会导致应用程序的整体性能还不如直接使用“产生线程”的方式。
为性能而使用并发就像所有其他优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可能使代码复杂化,使其更难理解,并更容易出错。因此,只有应用中具有显著增益潜力的性能关键部分,才值得并发化。当然,如果性能收益的潜力仅次于设计清晰或关注点分离,可能也值得使用多线程设计。

C++多线程

C++多线程历史

C++98(1998)标准不承认线程的存在,并且各种语言要素的操作效果都以顺序抽象机的形式编写。不仅如此,内存模型也没有正式定义,所以在C++98标准下,没办法在缺少编译器相关扩展的情况下编写多线程应用程序。
当然,编译器供应商可以自由地向语言添加扩展,添加C语言中流行的多线程API———POSIX标准中的C标准和Microsoft Windows API中的那些———这就使得很多C++编译器供应商通过各种平台相关扩展来支持多线程。

新标准支持并发

所有的这些随着C++11标准的发布而改变了,新标准中不仅有了一个全新的线程感知内存模型,C++标准库也扩展了:包含了用于管理线程、保护共享数据、线程间同步操作,以及低级原子操作的各种类。

Hello World示例

#include <iostream>
int main()
{
  std::cout << "Hello World\n";
}

采用C++11多线程库并发打印Hello World

#include <iostream>
#include <thread>  //1
void hello()  //2
{
  std::cout << "Hello Concurrent World\n";
}
int main()
{
  std::thread t(hello);  //3
  t.join();  //4
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值