C++ 并发 Hello World
这个章节包含下面的几个话题
- 什么是并发和多线程
- 你为什么要在你的程序中使用并发和多线程
- C++ 并发支持的历史
- 一个简单的C++多线程程序是什么样的
什么是并发
最简单和基础的层次来说, 并发是两个或者两个以上的独立活动在同一时刻发生. 譬如我们能够一般走路一边说话, 一双手可以同时处理不同的事. 当然我们每个人也在同时做着不同的事情- 你可以看足球而我正在游泳.
计算机系统中的并发
Figure1.1 展示了计算机中恰好两个任务在运行的场景, 每个task拆分为10个大小相同的块; 在一个双核的集器上(双核), 每个任务可以在自己的核上运行. 在一个单核集器上两个任务是相互切换的, 被切分的块交错运行. 但他们交错运行时会发呆一会(图中灰色的块切分了任务块), 为了能够交错运行任务块, 系统不得不进行上下文切换, 从一个任务切换到另一个任务, 这会消耗一部分时间. 为了能够进行上下文切换, OS需要保存当前任务CPU状态和指令指针, 找出需要切换的任务, 然后加载要切换任务的CPU状态. CPU还会潜在的去加载指令和数据到内存中, 在此期间CPU是不会执行其它指令的, 这造成了延时.
即使一个系统支持物理的并发(譬如双核), 也会有超过硬件并发数的任务需要并发运行, 所以任务切换仍然是必须的.
并发的方式
多进程并发
这种方式是将应用拆分为多个能够同时运行的独立的单线程进程.就像你可以同时打开多个浏览器和多个word. 这些独立的进程相互之间可以通过常规的进程间通信手段(信号, sockets, 文件, 管道等)传递消息. 缺点就是常常建立起来比较复杂, 慢或者两个缺点都有, 操作系统通常都提供了许多保护手段来防止一个进程修改另一个的数据. 另一个缺点是花时间来启动一个进程, 操作系统需要消耗内部资源来管理进程等等
优点, 更容易地写出安全地并发代码
多线程并发
这种方式是在单核生运行多线程, 线程更像是轻量级地进程: 每个线程独立运行, 线程之间可以有不同地指令执行顺序.但是线程共享进程中相同地地址空间, 大部分数据可以被所有线程访问.
共享内存地灵活性也带来了代价: 如果数据被多线程访问, 应用开发者必须保证数据地一致性. 围绕线程共享数据地问题, 第3,4,5和8章提供了一些工具和准则来避免.
为什么使用并发
- 功能拆分
将相关地代码放在一起, 不相关地剥离开, 可以让你地程序更容易地理解和测试, 可能更少地bug - 性能
- 将一个任务拆分成及部分然后并行运行他们, 这样减少了整体运行时间, 这称之为任务并行化
- 使用可用的并发解决大数据问题; 相比于一个时间处理一个文件, 处理2个. 10或者20 更有效率. 这称之为数据并行化
- 什么时候不用并发?
当代价高于收益时
C++中的并发和多线程
对于C++来说, 多线程并发的标准支持是新事物. 只有C++11标准或之后, 你才能写多线程代码.
C++多线程的历史
新标准并发支持
C++线程库的效率
相比于直接使用底层设施, 使用高层级的设施实现C++线程库性能消耗是在: 抽象惩罚.
C++ 标准委员会在设计C++标准库的时候非常在意性能问题, 尤其是C++ 线程库.
- 其中一个设计目标就是直接使用底层底层实现的好处相比于C++线程库没有或者只有一点点
- 另一个目的是确保C++ 提供足够的底层工具/设施给那些挑战极限性能的人
平台相关的工具
为了不放弃标准C++多线程库以及平台给我们带来的便利, C++标准库提供了native_handle() 成员函数, 允许在其中直接实现平台相关的操作. 所有使用native_handle()相关的操作都是完全平台相关的, 不在本书的范围
开始构建一个简单的多线程应用
Hello, 并发世界
- C++ 标准库多线程支持; 管理线程的函数和库都在头文件中声明, 而保护共享数据的函数和类则在其它头文件中声明
- 每个线程必须又一个初始函数, 这是新线程执行开始的地方. 在应用中初始线程,这里是main(), 但是在由std::thread对象构造的线程, 在这个例子中, std::thread 对象命名为t 有一个初始化函数hello().
- 新线程(t)初始化后, 初始化线程(main)继续执行. 如果不等待新线程结束, main()继续执行, 整个应用结束. 有可能新线程都没机会执行, 这也是为什么会在4步骤加joint()的原因
- 等待t 线程执行结束