C++ 多线程并发编程1 基础

1. 进程与线程

进程是cpu资源分配的最小单位,线程是cpu调度的最小单位

我们可以将程序定义为对资源进行计算的过程。在早期的计算机中并没有线程的概念,进程就是最基本的运行单位,它在拥有资源所有权,负责对资源进行隔离的同时,也执行计算任务。这是因为早期的CPU只有一个物理核心,也没有逻辑核心和超线程的概念,在物理上只能同时执行一个计算任务。但是随着计算机技术的发展,CPU的核心数越来越多,单核的算力也越来越强,同时也实现了逻辑核心,可以做到同时执行多个计算任务,这个时候如果还像早期进程那样,将资源和计算耦合在一起,当存在大量细粒度的并行计算任务,且都要访问同一块共享资源时,进程间的通信和切换会带来很大的额外开销。

因此为了解决资源隔离和并行计算之间的矛盾,人们在进程的基础上引入了线程,将资源和计算在一定程度上解耦

  • 进程只有用资源的所有权,负责对资源的隔离与管理,并不执行具体的计算任务。
  • 同时每个进程都拥有一到多个线程,用于执行具体的计算任务,其中有一个线程是进程的主线程。同一进程的不同线程共享该进程的地址空间、打开的文件描述符、信号处理器等大部分 OS 资源,同时每个线程拥有自己的栈、寄存器上下文、线程局部存储和线程调度属性等私有资源。
  • 这样就避免了大量进程切换带来的地址空间切换开销,同时线程间共享内存,通信开销也更小

2. 同步与异步

同步和异步的区别在于,当调用者发起操作时,是否会阻塞直到这个操作完成再去执行后续的代码。

  • 同步(Synchronous)
    • 调用者发起操作后,会阻塞等待该操作完成,才能继续执行后续代码。
  • 异步(Asynchronous)
    • 调用者发起操作后,不等待操作完成,立即继续执行后续代码。
    • 一般用于I/O操作等耗时较长的任务
    • 异步调用分为两种情况
      • 调用方不关心操作的执行结果
      • 调用方需要知道操作的执行结果
        这种情况有两种实现方式
        • 通知机制 即当任务执行完成后发送信号来通知调用方任务完成,这里的信号有很多实现方式,例如Linux中的signal,或者使用信号量等机制都可以实现。
        • 回调 即在异步调用时传入一个函数指针或可调用对象,当异步操作完成时由负责异步操作的线程来调用它。

3. std::thread

C++11引入了std::thread类来创建和管理线程

1
#include<thread>

3.1 构造函数

  • 默认构造函数
    1
    thread() noexcept;
    创建一个空的 thread 执行对象。
  • 初始化构造函数
    1
    2
    template <class Fn, class... Args>
    explicit thread (Fn&& fn, Args&&... args);
    创建一个thread对象,该thread对象可被joinable,新产生的线程会调用fn函数,该函数的参数由args给出。常用以下的初始化方式
    • std::thread th1(proc1);
      创建了一个名为th1的线程,程序执行此语句后线程th1就已经开始执行函数proc1
    • std::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;
  • 移动赋值函数
    1
    thread& operator= (thread&& rhs) noexcept;
    如果当前对象可被 joinable,则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 其他成员函数

参考


C++ 多线程并发编程1 基础
https://guts.homes/2025/06/23/cpp-multiThreading1/
作者
guts
发布于
2025年6月23日
许可协议