读《C++并发编程实战 -- C++ Concurrency In Action》

C++ 并发编程基础

什么是并发

并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务并不会同时运行。

并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。

并发的方式包括多进程并发和多线程并发,多进程并发通过进程间通信(信号、套接字、文件、管道等)来相互传递信息;由于同一进程内的所有线程都共用相同的地址空间,所以多进程并发通过共享内存来同步数据。

并发技术可以:

  1. 分离关注点(separation of concerns),使得不同的线程关注不同的任务;
  2. 提升性能。任务并行可以采取两种方式,一种是将单一任务分成多个部分,各自并行运作,从而节省总运行耗时;第二种是利用并行资源解决规模更大的问题。

什么时候避免并发?

  1. 并发会增加额外的复杂度,增加开发时间和维护成本
  2. 多线程的性能增幅可能不如预期,因为线程的启动会有额外的时间开销
  3. 线程是一种有限的资源,过多的线程会消耗系统资源,从而导致系统整体变慢
  4. 运行的线程越多,操作系统的上下文切换就越频繁,上下文切换会减少本该用于实质工作的时间

我们先尝试一个简单的多线程版本的 Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <thread>

using namespace std;

void hello() {
cout << "Hello Concurrent World\n";
}

int main(){
thread t(hello);
t.join();
}

线程管控

线程的管控可以通过 std::thread 对象来实现。

悬空引用(Dangling Reference)是指一个引用在指向有效数据之后,被引用的数据被销毁或释放,从而引用变成了无效的情况。这种情况可能导致程序运行时的未定义行为,因为引用指向的数据已经不存在,但程序仍试图通过该引用访问这个不存在的数据。如果新线程上的函数持有指针或引用,指向主线程的局部变量;但主线程所运行的函数退出后,新线程却还没结束,这时就会访问悬空引用的情况。

上述情况的处理办法是令线程函数完全自含(self-contained),将数据复制到新线程内部,而不是共享数据。另一种方法是汇合新线程,确保主线程的函数退出前,新线程执行完毕。

等待线程完成可调用成员函数join()来实现。在std::thread对象销毁前,我们需确保已经调用join()detach().假使打算等待线程结束,则需小心地选择执行代码的位置来调用join()。原因是,如果线程启动以后主线程有异常抛出,而join()尚未执行,则该join()调用会被略过。

在线程间共享数据

并发操作的同步

C++内存模型和原子操作

设计基于锁的并发数据结构

设计无锁数据结构

设计并发代码

高级线程管理

并行算法函数

多线程应用的测试和除错