一、认识并发和并行
先将两个概念,
并发与并行
并发:同一时间段内可以交替处理多个操作:
图中整个安检系统是一个并发设计的结构。两个安检队列队首的人竞争这一个安检窗口,两个队列可能约定交替着进行安检,也可能是大家同时竞争安检窗口(通信)。后一种方式可能引起冲突:因为无法同时进行两个安检操作。在逻辑上看来,这个安检窗口是同时处理这两个队列
并行:同一时刻内同时处理多个操作:
图中整个安检系统是一个并行的系统。在这里,每个队列都有自己的安检窗口,两个队列中间没有竞争关系,队列中的某个排队者只需等待队列前面的人安检完成,然后再轮到自己安检。在物理上,安检窗口同时处理这两个队列。
并发的程序设计,提供了一种方式让我们能够设计出一种方案将问题(非必须地)并行地解决。如果我们将程序的结构设计为可以并发执行的,那么在支持并行的机器上,我们可以将程序并行地执行。因此,并发重点指的是程序的设计结构,而并行指的是程序运行的状态。并发编程,是一种将一个程序分解成小片段独立执行的程序设计方法。
从线程的角度看
维基百科对线程的定义是:线程是一个编排好的指令序列,这个指令序列(线程)可以和其它的指令序列(线程)并行执行,操作系统调度器将线程作为最小的 CPU 调度单元。
图 1、单个 CPU 内核上的多个线程运行示意图
我们可以看到,这时的多线程本质上是单个 CPU 的时间分片,一个时间片运行一个线程的代码,它可以支持并发处理,但是不能说是真正的并行计算。
图 2、双核 CPU 上的多个线程运行示意图
从上述两图,我们可以直接得到使用多线程的一些常见场景:
- 进程中的某个线程执行了一个阻塞操作时,其它线程可以依然运行,比如,等待用户输入或者等待网络数据包的时候处理启动后台线程处理业务,或者在一个游戏引擎中,一个线程等待用户的交互动作输入,另外一个线程在后台合成下一帧要画的图像或者播放背景音乐等。(异步?)
- 将某个任务分解为小的可以并行进行的子任务,让这些子任务在不同的 CPU 或者内核上同时进行计算,然后汇总结果,比如归并排序,或者分段查找,这样子来提高任务的执行速度。
二、并发的基本方式途径
多线程与多进程是并发的两种途径。
前面也讲到了并发的两个场景:非阻塞、并行执行子任务
多进程并发
多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。
运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。
多线程并发
在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。
三、C++11中的并发与多线程
C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API——这需要依赖相关平台。
C++11 标准提供了一个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。标准极大地提高了程序的可移植性,以前的多线程依赖于具体的平台,而现在有了统一的接口进行实现。
C++11 新标准中引入了几个头文件来支持多线程编程:
- < thread > :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在 中声明.
- < atomic > :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。
- < mutex > :包含了与互斥量相关的类以及其他类型和函数
- < future > :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。
- < condition_variable > :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。
1. 第一个程序
我们从一个hello开始。在单线程时:
#include<iostream> using namespace std; int main() { cout<<"hello world"<<endl; }
在这里,进程由一个线程组成,该线程的初始函数是main。我们启动第二个线程来打印hello world:
# include<iostream> # include<thread> using namespace std; void hello() { cout<<"hello world"<<endl; } int main() { thread t(hello); t.join(); }
每个线程都必须有一个初始函数,新线程的执行开始于初始函数;
程序由两个线程组成:初始线程始于main,新线程始于hello。这里将新线程t的初始函数指定为hello;
新线程启动之后会与初始进程一并运行,初始线程可以等待或不等待新进程的运行结束——如果需要等待线程,则新线程实例需要使用join(),否则可以使用detach()。如果不等待新线程,则初始线程自顾自地运行到main()结束。
2. 深入学习
在我们编写出的第一个多线程的程序,一般来说并不值得为了如此简单的任务而使用多线程,尤其是在这期间初始线程并没做什么。
参考链接: