C++ 多线程并发编程1 基础
1. 进程与线程
进程是cpu资源分配的最小单位,线程是cpu调度的最小单位
我们可以将程序定义为对资源进行计算的过程。在早期的计算机中并没有线程的概念,进程就是最基本的运行单位,它在拥有资源所有权,负责对资源进行隔离的同时,也执行计算任务。这是因为早期的CPU只有一个物理核心,也没有逻辑核心和超线程的概念,在物理上只能同时执行一个计算任务。但是随着计算机技术的发展,CPU的核心数越来越多,单核的算力也越来越强,同时也实现了逻辑核心,可以做到同时执行多个计算任务,这个时候如果还像早期进程那样,将资源和计算耦合在一起,当存在大量细粒度的并行计算任务,且都要访问同一块共享资源时,进程间的通信和切换会带来很大的额外开销。
因此为了解决资源隔离和并行计算之间的矛盾,人们在进程的基础上引入了线程,将资源和计算在一定程度上解耦。
- 进程只有用资源的所有权,负责对资源的隔离与管理,并不执行具体的计算任务。
- 同时每个进程都拥有一到多个线程,用于执行具体的计算任务,其中有一个线程是进程的主线程。同一进程的不同线程共享该进程的地址空间、打开的文件描述符、信号处理器等大部分 OS 资源,同时每个线程拥有自己的栈、寄存器上下文、线程局部存储和线程调度属性等私有资源。
- 这样就避免了大量进程切换带来的地址空间切换开销,同时线程间共享内存,通信开销也更小。
2. 同步与异步
同步和异步的区别在于,当调用者发起操作时,是否会阻塞直到这个操作完成再去执行后续的代码。
- 同步(Synchronous)
- 调用者发起操作后,会阻塞等待该操作完成,才能继续执行后续代码。
- 异步(Asynchronous)
- 调用者发起操作后,不等待操作完成,立即继续执行后续代码。
- 一般用于I/O操作等耗时较长的任务
- 异步调用分为两种情况
- 调用方不关心操作的执行结果
- 调用方需要知道操作的执行结果
这种情况有两种实现方式- 通知机制 即当任务执行完成后发送信号来通知调用方任务完成,这里的信号有很多实现方式,例如
Linux中的signal,或者使用信号量等机制都可以实现。 - 回调 即在异步调用时传入一个函数指针或可调用对象,当异步操作完成时由负责异步操作的线程来调用它。
- 通知机制 即当任务执行完成后发送信号来通知调用方任务完成,这里的信号有很多实现方式,例如
3. std::thread类
C++11引入了std::thread类来创建和管理线程
1 | |
3.1 构造函数
- 默认构造函数 创建一个空的
1
thread() noexcept;thread执行对象。 - 初始化构造函数 创建一个
1
2template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);thread对象,该thread对象可被joinable,新产生的线程会调用fn函数,该函数的参数由args给出。常用以下的初始化方式std::thread th1(proc1);
创建了一个名为th1的线程,程序执行此语句后线程th1就已经开始执行函数proc1std::thread th2(proc2, a, b);
线程th2用于执行函数void proc2(int a, int b)std::thread th3(&Utils::proc3, a, b);
线程th3用于执行类Utils的静态成员函数void Utils::proc3(int a, int b)。之所以要使用&,是因为std::thread的构造函数参数实际上是函数指针,其在内部也是使用函数指针来调用函数的,而普通函数的函数名根据上下文会自动退化成函数指针,类的静态函数本质上也是“挂在类名下的普通函数”,也会退化成函数指针,但类的非静态成员函数不会,需要使用&直接对其取地址。(对于普通函数和静态成员函数,也可以使用&取地址再传参)
- 拷贝构造函数 被禁用
1
thread (const thread&) = delete; - 移动构造函数 调用成功之后
1
thread (thread&& x) noexcept;x不代表任何thread执行对象。
所有的std::thread对象都是异步执行的。
3.2 赋值函数
- 赋值构造函数 被禁用
1
thread& operator= (const thread&) = delete; - 移动赋值函数 如果当前对象可被 joinable,则
1
thread& operator= (thread&& rhs) noexcept;std::terminate()报错。
3.3 join()与detach()
join()与detach()都是std::thread类的成员函数,是两种线程阻塞方法,区别在于是否等待子线程执行结束。
需要注意的是线程在 std::thread 对象被成功构造的那一刻,就已经开始执行了。操作系统会立即(或在很短的时间内)将其纳入调度,分配CPU时间片。它不是在调用 join() 或 detach() 之后才开始执行的。join() 或 detach()只是用来**决定主线程如何与新线程“告别”**的
join()join()等待调用线程运行结束后当前线程再继续运行,例如在主函数中调用th1.join(),会阻塞主函数,直到线程th1结束运行,主函数再继续运行,以确保能够正确释放线程占用的资源。- 需要注意阻塞是从
join()的调用而非std::thread对象的创建开始的
detach()detach()则是执行完后立即执行主函数中下一行代码detach()会让线程脱离std::thread对象管理,变成“后台线程”,它结束时会自动清理自身资源,而主线程不再负责其生命周期。
当线程启动后,一定要在和线程相关联的std::thread对象销毁前,对线程运用join()或者detach()方法。这是因为 std::thread 对象在析构时,如果仍然是 “joinable”(也就是既没有被 join() 也没有被 detach()),就会调用 std::terminate() 导致程序直接终止。这样的设计是为了明确所创建线程的生命周期和资源管理由谁负责,如果既不调用join()也没有调用detach(),那么析构函数就不知道该如何处理这个线程。
3.4 其他成员函数
get_id()
获取线程的IDjoinable()
检查线程是否是joinable的swap()native_handle()
返回native handle。hardware_concurrency()
检测硬件并发特性