第一章 你好,C++的并发世界!
本章主要内容:
- 什么是并发和多线程
- 引用程序为什么要使用并发和多线程
- C++ 的并发史
- 一个简单的C++多线程程序
1.1 什么是并发
1.1.1 计算机系统中的并发
简单地说,一边走路一边说话就是一种并发。
而在计算机领域,并发指的是在单个系统里同时执行多个独立的任务,而非顺序的进行一些活动。 但实际上,处理器并不能做到同时处理多个任务,而是这个任务坐一会儿,那个任务坐一会儿,让任务看起来是在并行执行。这种方式称为“任务切换(task switching)”。如今,我们仍然将这样的系统称为并发(concurrency):因为任务切换得太快,以至于无法感觉到任务在何时会被暂时挂起,而切换到另一个任务。任务切换会给用户和应用程序造成一种“并发的假象”。因为这种假象,当应用在任务切换的环境下和真正并发环境下执行相比,行为还是有着微妙的不同。
- 硬件并发(hardware threads):通过多核处理器来实现真正的并行处理多个任务。
在单核处理器中,通过任务切换实现并发,为了做到这一点,系统每一次从一个任务切换到另一个任务都需要进行一次上下文切换(context switch),会消耗一定的时间。进行上下文的切换时,操作系统必须为当前运行的任务保存CPU的状态和指令指针,并计算出要切换到哪个任务,并为即将切换到的任务重新加载处理器状态。然后,CPU可能要将新任务的指令和数据的内存载入到缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟。
实际场景中,硬件并发与上下文切换是同时存在的。
1.1.2 并发的途径
一个很不错的理解线程与进程的例子
试想当两个程序员在两个独立的办公室一起做一个软件项目,他们可以安静地工作、不互相干扰,并且他们人手一套参考手册。但是,他们沟通起来就有些困难,比起可以直接互相交谈,他们必须使用电话、电子邮件或到对方的办公室进行直接交流。并且,管理两个办公室需要有一定的经费支出,还需要购买多份参考手册。
假设,让开发人员同在一间办公室办公,他们可以自由的对某个应用程序设计进行讨论,也可以在纸或白板上轻易的绘制图表,对设计观点进行辅助性阐释。现在,你只需要管理一个办公室,只要有一套参考资料就够了。遗憾的是,开发人员可能难以集中注意力,并且还可能存在资源共享的问题(比如,“参考手册哪去了?”)
以上两种方法,描绘了并发的两种基本途径。每个开发人员代表一个线程,每个办公室代表一个处理器。第一种途径是有多个单线程的进程,这就类似让每个开发人员拥有自己的办公室,而第二种途径是在单一进程里有多个线程,如同一个办公室里有两个开发人员。
多进程并发
独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、文件、管道等等)。不过,这种进程之间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以避免一个进程去修改另一个进程的数据。还有一个缺点是,运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程,等等。
使用独立的进程实现并发编程有一个额外的优势——可以使用远程连接的方式,在不同的机器上运行独立的进程。
多线程并发
并发的另一个途径,在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立运行,且线程可以在不同的指令序列中运行。但是,进程中的所有线程都共享地址空间,并且所有线程访问到大部分数据——全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。 虽然,进程之间通常共享内存,但这种共享通常也是难以建立,且难以管理。因为,同一数据的内存地址在不同的进程中是不相同。
1.2 为什么使用并发?
两个原因:关注点分离(SOC)和性能
1.2.1 为了分离关注点
比如一个线程处理UI逻辑,另一个线程处理数据操作。如最初接触多线程时写的一篇文章# 多线程初探:在 Qt 应用中实现多线程文件搜索
1.2.2 为了性能
两种方式利用并发提高性能:
- 任务并行(_task parallelism_):将一个单任务分成几部分,且各自并行执行,从而降低总运行时间 (较为复杂,任务的各部分可能存在依赖关系)
- 使用可并行的方式,来解决更大的问题。与其同时处理一个文件,不如酌情处理2个、10个或20个。虽然,这是数据并行的一种应用(通过对多组数据同时执行相同的操作),但着重点不同。处理一个数据块仍然需要同样的时间,但在相同的时间内处理了更多的数据。例如,并行处理图片的各个部分。
1.2.3 什么时候不使用并发
知道何时不使用并发与知道何时使用它一样重要。基本上,不使用并发的唯一原因就是,收益比不上成本。
- 收益成本考量:并发代码难理解和维护,编写及维护多线程代码有脑力成本,额外复杂性易引发更多错误。若潜在性能增益不够大或关注点分离优势不足以抵消额外开发与维护成本(在代码正确前提下),则不使用。
- 性能增益未达预期:启动线程有固有开销,包括分配内核资源、堆栈空间和加入调度器等需时间。若任务完成快,任务执行时间远小于启动线程时间,整体性能可能不如不产生线程的方式。
- 线程资源有限:线程是有限资源,太多线程运行消耗操作系统资源使系统变慢。线程池虽可限制线程数量,但也有自身问题。
- C/S 应用场景:客户端/服务器应用在服务器端为每个连接启动独立线程,少量连接可行,但处理大量连接时,因线程多会耗尽系统资源,此时谨慎使用线程池可优化性能。
- 上下文切换开销:运行越多线程,操作系统上下文切换越多,每次切换耗费时间,可能使增加额外线程降低而非提高应用程序整体性能。
1.3 C++中的并发和多线程
1.3.1 C++多线程历史
1.3.2 新标准支持并发
C++11标准库扩展了:包含了用于管理线程(参见第2章)、保护共享数据(参见第3章)、线程间同步操作(参见第4章),以及低级原子操作(参见第5章)的各种类。
1.3.3 C++线程库的效率
1.3.4 平台相关的工具
1.4 开始入门
1.4.1 你好,并发世界
1 |
|