1. 基础概念

多线程编程之前,先搞清楚几个基本概念:并发、并行、进程、线程、协程。它们经常被混用,但含义差别很大,而且这些差异最终都落在硬件层面。


1.1 并发与并行

并发(concurrency)指多个任务在同一时间段内交替推进——宏观上看它们"同时"在进行,微观上可能只有一个执行单元在轮流处理。并行(parallelism)指多个任务在同一时刻真正同时执行,这要求有多个执行单元(多个 CPU 核心)。

两者的关系可以用一个经典表述概括:并发是程序结构层面的性质,并行是执行层面的性质。一个程序可以设计成并发的(多个任务交替推进),但只有在多核机器上运行时才可能并行。

单核并发(时间片轮流):

CPU 核心:  [任务A][任务B][任务A][任务B][任务A][任务B] ...
           └── 每个时刻只有 1 个任务在跑 ──┘

双核并行:

核心 1:  [任务A][任务A][任务A][任务A] ...
核心 2:  [任务B][任务B][任务B][任务B] ...
           └── 同一时刻有 2 个任务在跑 ──┘

注意:并发程序即使只有单核也能写(靠调度器切换),而并行程序必须有多核硬件。反过来,并发也是并行的前提——没有把程序拆成多个可独立推进的任务,再多核心也只有一个任务在跑。


1.2 进程

进程(process)是操作系统进行资源分配的基本单位。每个进程拥有独立的虚拟地址空间:自己的代码段、数据段、堆、栈,还有独立的文件描述符表、信号处理器等资源。

进程之间天然隔离,一个进程崩溃不会直接拖垮另一个进程。但也正因隔离,进程间要交换数据必须走进程间通信(IPC,Inter-Process Communication)机制:管道、消息队列、共享内存、套接字等,这些机制要么有系统调用开销,要么需要额外的同步。

创建进程的开销比较大:Linux 上 fork() 要复制页表、文件描述符表、信号处理等内核数据结构(写时复制技术只缓解了内存页的复制),Windows 上 CreateProcess 更是要完整加载一个新的地址空间。


1.3 线程

线程(thread)是操作系统进行调度的基本单位,是进程内部的一条独立执行流。一个进程至少有一个线程(主线程),也可以有多个线程。

同一进程内的所有线程共享进程的地址空间:全局变量、堆内存、文件描述符都能直接访问,不需要 IPC。每个线程私有的东西只有:寄存器状态、栈(所以局部变量是线程私有的)、程序计数器、线程局部存储。

进程 A 的内存布局:

┌─────────────────────────────┐
│ 代码段 / 全局数据 / 堆       │  ← 所有线程共享
├──────────────┬──────────────┤
│ 线程 1 的栈  │ 线程 2 的栈  │  ← 每个线程独立
├──────────────┴──────────────┤
│ 线程 1 的寄存器、PC、TLS     │
│ 线程 2 的寄存器、PC、TLS     │
└─────────────────────────────┘

由于共享地址空间,线程之间交换数据只需要直接读写共享变量,代价远低于 IPC;但共享也意味着互相干扰的风险——两个线程同时改同一个变量就会出问题(详见《数据共享问题与锁》)。


1.4 进程与线程的对比

对比项 进程 线程
地址空间 相互独立 同进程内共享
资源开销 大(页表、内核结构) 小(只需一个栈和少量内核结构)
通信方式 IPC(管道、共享内存等) 直接读写共享变量
调度单位 进程(内核调度) 线程(内核调度)
切换开销 大(地址空间切换,TLB 失效) 较小(同一地址空间内切换)
隔离性 强,崩溃互不影响 弱,一个线程崩溃(如非法访问)整个进程退出
适用场景 需要强隔离、多程序 需要频繁协作、共享数据

注:严格来说 Linux 的调度单位是线程,fork 出来的子进程和线程在内核里都对应 task_struct,区别在于是否共享地址空间。所以"进程切换开销大"的本质是地址空间切换(TLB 缓存失效)带来的,而不是调度本身。


1.5 协程

协程(coroutine)是用户态的轻量级执行流:它由程序自己(或运行时库)调度,不经过操作系统内核。协程的切换只发生在用户态,保存/恢复少量寄存器即可,不需要陷入内核,所以切换开销比线程低一两个数量级。

一个线程上可以挂很多协程,它们轮流使用这个线程的算力。协程的特点是挂起(suspend)时主动让出执行权,等条件满足再恢复(resume)。C++20 引入了语言级协程支持(co_await、co_yield 等),但需要配合库(如 Boost.Asio)使用才方便。

协程与线程不是替代关系:协程跑在线程上。一个多线程程序里,每个线程都可以运行自己的协程调度器。可以这样理解层级:

硬件线程(CPU 核心/逻辑核心)
  └── 内核线程(操作系统调度)
        └── 协程(用户态调度,跑在线程上)

注意:协程的挂起/恢复是在协作式前提下工作的——一个协程如果不主动让出,同线程的其他协程就永远得不到执行。线程则是抢占式调度,内核随时可能切换它。


1.6 硬件视角:CPU 核心与超线程

并发/并行最终要落到硬件上。现代 CPU 的几个关键事实:

1. 物理核心(core):一个物理核心包含完整的执行单元(算术逻辑单元、浮点单元等),能独立执行指令流。有多少个物理核心,就最多支持多少个真正的并行执行流。

2. 超线程(SMT,同步多线程):Intel 的超线程技术让一个物理核心对外表现为两个逻辑核心——两个执行流共享同一套执行单元,但各自有独立的寄存器状态和中断处理。当一条指令流在执行单元上停顿(比如等缓存命中)时,另一条可以趁机利用执行单元。所以:

  • 逻辑核心数 = 物理核心数 × 每核心线程数(通常 2)
  • std::thread::hardware_concurrency() 返回的就是逻辑核心数
  • 超线程对吞吐量有帮助,但对单线程性能没有提升,某些场景甚至略有下降

3. 缓存层级:每个核心有自己的 L1、L2 缓存,多个核心共享 L3 缓存,最后才是主内存。每个核心看到的内存视图可能不一致——核心 A 改了变量,核心 B 的缓存里还是旧值。这靠缓存一致性协议(如 MESI)来协调,代价是跨核心同步数据比单核读写慢得多(详见《原子操作》)。

4. 伪共享(false sharing):缓存以缓存行(通常 64 字节)为单位加载。如果两个线程频繁读写不同的变量,但这两个变量恰好落在同一条缓存行上,缓存一致性协议会让这两个核心不断互相失效缓存行,性能急剧下降。这是多线程性能优化中最隐蔽的坑之一(《原子操作》中会展开)。

注:判断"该开多少个线程"时,参考 hardware_concurrency(),但超线程带来的逻辑核心对计算密集任务帮助有限,IO 密集任务则往往可以开更多线程。


1.7 多线程的资源共享

多线程的核心优势来自共享:同一进程的线程直接共享全局变量、堆内存。共享的收益是:

  • 数据交换零拷贝、零系统调用,读写一个共享变量就能通信
  • 天然适合"多个任务协作处理同一份数据"的场景(如并行求和、生产者消费者)

代价是正确性问题:多个线程同时读写同一内存位置时,执行顺序不确定,结果不确定。这种"多个执行流竞争访问同一数据"的情况统称数据竞争(data race),在 C++ 中属于未定义行为(UB,Undefined Behavior)。

C++ 标准对数据竞争的界定很严格:

如果两个线程同时访问同一内存位置,其中至少一个是写操作,且两者之间没有任何同步关系(锁、原子操作等),就是数据竞争,程序行为未定义。

"未定义"意味着编译器可以任意优化:变量可能被缓存到寄存器里不写回内存、读写可能被重排、循环可能被拆掉——程序跑出任何结果都不违反标准。所以多线程编程的第一原则是:凡是共享的数据,必须用同步机制(锁或原子操作)保护。这些内容在后续文章中详细展开。


1.8 多线程的开销

多线程不是免费的。每创建一个线程、每切换一次执行流都有真实开销:

1. 创建与销毁开销:创建线程需要系统调用,内核要分配线程栈(Linux 默认 8 MB 虚拟内存,Windows 默认 1 MB)、初始化调度器数据结构。高频创建/销毁线程非常浪费,所以工程上常用线程池复用线程(《多线程编程进阶》会实现一个)。

2. 上下文切换开销:线程切换时内核要保存当前线程的寄存器、程序计数器,恢复目标线程的,还要刷新部分缓存/TLB。一次切换大约几微秒,看似不多,但切换后缓存是冷的(cold cache),新线程要重新把数据加载进缓存,这个代价常常比切换本身还大。

3. 同步开销:锁、原子操作都会引入额外的指令和缓存同步流量。互斥锁的加解锁在无竞争时大约几十纳秒,有竞争时可能让线程进入睡眠、再被唤醒,耗时达到微秒甚至毫秒级。

4. 内存开销:每个线程独立的栈占虚拟内存(按需提交物理页,实际用量取决于栈深度),成千上万个线程会耗尽虚拟地址空间或触发系统线程数上限。

三种执行流开销对比(量级参考,平台相关):

执行流 创建开销 切换开销 调度者
进程 高(毫秒级) 高(地址空间切换) 内核
线程 中(几十微秒级) 中(几微秒) 内核
协程 低(几百纳秒级) 极低(用户态,纳秒级) 用户态运行时

注意:这些开销只有在任务量远大于切换成本时才值得用多线程。任务本身只有几微秒却频繁切换线程,性能反而更差。


1.9 多线程的优缺点

优点:

  • 利用多核:把计算拆成多个独立部分并行执行,是单线程无法获得的吞吐提升
  • 隐藏延迟:一个线程阻塞在 IO 上时(磁盘、网络),其他线程继续用 CPU,提高整体利用率
  • 提升响应性:GUI 程序把耗时任务放到后台线程,界面不卡顿
  • 天然适合协作:共享内存模型下,生产者/消费者这类协作模式实现直接

缺点:

  • 正确性风险:数据竞争、死锁、活锁、饥饿,任何一个都是难以定位的 bug
  • 非确定性:线程调度顺序由内核决定,同一份代码每次运行结果可能不同,bug 难以复现
  • 调试困难:断点会改变时序,单步调试多线程程序往往"一调就好"
  • 额外开销:创建、切换、同步都有成本,小任务用多线程得不偿失
  • 复杂度:并发程序的推理难度远高于单线程,代码更难维护

适用判断:任务可以拆成多个独立子任务,且每个子任务足够大(大到抵消切换开销)、子任务间共享数据少或同步成本可控——这时多线程是值得的。反过来,任务小、依赖强、对确定性要求极高,就保持单线程。


2. C++11 多线程编程

C++11 之前,C++ 标准库没有线程支持,多线程程序要么用 POSIX 线程(pthread,Linux/macOS),要么用 Win32 线程 API(Windows),代码不可移植。C++11 在标准库中引入了 <thread> 等头文件,从此可以用一套标准 API 写跨平台多线程代码。

本篇文章给出最基础的用法:线程的创建、启动、等待、结束。更深入的内容(传参、同步、进阶设施)在系列后续文章中展开。


2.1 第一个线程示例

std::thread 是标准库提供的线程类,构造它就会创建并启动一个线程,线程入口是传入的可调用对象:

#include <iostream>
#include <thread>

void hello()
{
    std::cout << "这里是一个子线程,线程id为 " << std::this_thread::get_id() << std::endl;
}

int main()
{
    std::cout << "程序开始" << std::endl;
    std::thread t(hello);
    t.join();

    std::cout << "主线程id为 " << std::this_thread::get_id() << std::endl;
    std::cout << "主线程结束" << std::endl; 

    return 0;
}

编译并运行,看到以下输出:

程序开始
这里是一个子线程,线程id为 0xb8
主线程id为 0xb8
主线程结束

2.2 线程入口:可调用对象

std::thread 的构造函数接受任何可调用对象(callable):普通函数、函数指针、lambda、仿函数、成员函数、std::bind 结果等。参数会按值拷贝到线程内部(传参细节见线程传参):

#include <iostream>
#include <thread>

void func(int x)
{
    std::cout << "这是一个函数,传入了一个 int 类型的参数: " << x << std::endl;
}

struct Functor
{
    void operator()(int x)
    {
        std::cout << "这是一个仿函数,传入了一个 int 类型的参数: " << x << std::endl;
    }

    void member_func(int x)
    {
        std::cout << "这是一个成员函数,传入了一个 int 类型的参数: " << x << std::endl;
    }
};

int main()
{
    // 1. 传入普通函数
    std::thread t1(func, 10);

    // 2. 传入 Lambda 表达式
    std::thread t2([](int x)
        { 
            std::cout << "这是一个 Lambda 表达式,传入了一个 int 类型的参数: " << x << std::endl; 
        }, 
        20);

    // 3. 传入仿函数
    std::thread t3(Functor{}, 30);

    // 4. 传入成员函数,需要先传入对象的地址(相当于第一个参数是 this 指针)
    Functor instance;
    std::thread t4(&Functor::member_func, &instance, 40);

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}

不难理解,成员函数的写法是 std::thread(&类::成员函数, 对象指针, 参数...),第一个参数是成员函数指针,第二个是调用它的对象地址,后续是实参。

运行后,得到以下输出:

这是一个函数,传入了一个 int 类型的参数: 10
这是一个 Lambda 表达式,传入了一个 int 类型的参数: 20
这是一个仿函数,传入了一个 int 类型的参数: 30
这是一个成员函数,传入了一个 int 类型的参数: 40

2.3 join:等待线程结束

join() 阻塞当前线程,直到目标线程执行完毕。join 的作用是同步:保证"线程 t 的代码已全部执行完"这个事实,在 join 返回后对当前线程可见。

#include <iostream>
#include <thread>

// 可以被所有线程访问的全局变量
int global_var = 10;

// 在线程中访问全局变量
void func()
{
    std::cout << "进入线程函数 func ,线程 id 为:" << std::this_thread::get_id() << std::endl;

    // 对全局变量进行修改
    global_var += 114514;
}

int main()
{
    std::thread t(func);    // 在 t 刚创建出来时,func 立即开始执行
    t.join();   // 主线程会在这里一直等待子线程 t 执行完毕

    std::cout << "线程 t 已经执行完毕,可以安全地访问它产生的结果:" << global_var << std::endl;

    return 0;
}

如果不 join,主线程可能先执行完 main 并退出——进程退出时所有线程都会被强制终止,子线程可能根本没跑完。所以凡是需要子线程完整执行完的场景,都必须 join。

注: 1. 一个用法示例:多个线程并行计算,最后汇总。先 join 所有线程,再读取共享结果:

#include <iostream>
#include <thread>
#include <vector>

int main()
{
    const int num_threads = 20;
    std::vector<std::thread> threads;
    std::vector<int> results(num_threads, 0);

    // 各个线程只写自己的结果,无数据竞争问题
    for(int i = 0; i < num_threads; ++i)
    {
        threads.emplace_back([i, &results] 
            {
                // 如果这里的计算操作非常耗时,就可以体现出多线程的优势
                results[i] = i * i;
            });
    }

    for(auto& t : threads)
    {
        t.join();                   // 全部结束后再统一读取结果
    }

    for(auto result : results)
    {
        std::cout << result << std::endl;
    }

    return 0;
}

2. 一个错误示例:join的不当使用

#include <iostream>
#include <thread>
#include <vector>

void do_something(int i)
{
    i++;i--; ++i;--i;
}

int main()
{
    std::vector<std::thread> threads;
    for(int i = 0; i < 100; ++i)
    {
        auto t = std::thread(do_something, i);
        t.join();   // 在这里进行 join 会直接阻塞,无法实现多线程并发执行

        // t只能移动,不能拷贝
        threads.push_back(std::move(t));
    }

    // 以上步骤相当于
    // std::thread t1(do_something, 0); t1.join();
    // std::thread t2(do_something, 1); t2.join();
    // ...
    // t1.join() 阻塞主线程直至 t1 执行完后,才会创建 t2,开始执行 t2
    // 以此类推
    // 完全没有起到并发执行的作用

    return 0;
}

2.4 detach:分离线程

detach() 把 std::thread 对象与它管理的线程解除关联:此后线程在后台继续运行,std::thread 对象不再代表任何线程,既不能对它 join,析构时也不会触发 terminate——它已经不持有线程了。线程的收尾交给运行时:执行完毕后自动释放资源,不需要任何人对它负责。

对比 join 来说:join 是"等待子线程结束";detach 则是 “不再关心子线程是否结束”。主线程调用 detach 后立刻继续执行自己的逻辑,甚至可以直接结束。这就是 detach 的作用——甩掉对线程的管理责任,让它作为后台任务自己跑完,通常称为fire-and-forget(似乎是可以翻译为 “发射后不管”,但是感觉怪怪的)。

#include <iostream>
#include <thread>

int main()
{
    std::thread t([] {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "后台线程执行完毕" << std::endl;
    });
    t.detach();      // 分离:主线程不再持有这个线程

    std::cout << "主线程继续执行, 不等待后台线程" << std::endl;
    return 0;
}

编译并运行,输出只有一行:

主线程继续执行, 不等待后台线程

"后台线程执行完毕"没有出现:后台线程要先睡 1 秒,而主线程打印完就 return 了,进程随即退出,后台线程被强制终止,永远醒不过来。

让主线程多活一会儿,就能看到后台线程的输出:

t.detach();

std::cout << "主线程继续执行" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));   // 进程保持存活
return 0;
主线程继续执行
后台线程执行完毕

两个版本唯一的区别是进程活了多久。这揭示了 detach 后台线程的真实处境:它能不能跑完,取决于进程什么时候退出。

进程退出时,所有线程——包括 detach 的——都会被强制终止。detach 只解除了 C++ 层面的管理关系,线程在操作系统层面始终属于进程,不存在"脱离进程独立存活"的线程。main 返回后依次发生:

  1. main 的局部变量按构造的逆序析构(此时后台线程还在跑)
  2. 全局/静态对象析构、atexit 注册的回调执行
  3. 运行时调用操作系统的进程终止接口(Linux 上是 exit_group 系统调用),内核把该进程的所有线程一并终止

第 3 步是内核层面的强杀:线程收不到任何通知,没有机会执行清理代码,栈上对象的析构函数不会运行,整个地址空间直接回收。所以 detach 的线程最多比 main 函数多活一小会儿(第 1、2 步的时间窗口),不可能比进程活得久。

回头看第一个示例:主线程 return 0 后,进程在第 3 步终止了还在睡觉的后台线程,std::cout 那行代码从未获得执行机会。这也说明"detach 然后指望它把活干完"常常是错的——要保证线程完整执行,进程就必须活着等它,这正是 join 存在的意义:

对比项 join() detach()
语义 等待线程执行完毕 放弃对线程的管理
调用后 std::thread 对象 变为非 joinable 变为非 joinable
线程能否保证执行完 能,join 返回时线程已结束 不能,进程退出即被终止
进程退出时 主线程阻塞在 join 处,不会退出 后台线程被强制终止
线程产生的结果 join 返回后可安全读取 无法确定何时结束,不能安全读取
典型用途 并行计算后汇总结果 与进程同生命周期的后台任务

那 detach 到底什么时候用?答案是线程本来就打算与进程同生共死的场景:后台日志线程、心跳上报线程、监控采样线程。它们通常是一个循环,主线程从头到尾都不打算等它,进程退出时被一并终止反而是期望行为:

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

std::atomic<bool> running{true};

void heartbeat()
{
    while (running.load())
    {
        // 周期性上报心跳(这里省略实际工作)
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main()
{
    std::thread hb(heartbeat);
    hb.detach();    // 主线程从没打算等心跳线程结束——它本来就不会结束

    for (int i = 0; i < 5; ++i)    // 主线程干自己的活
    {
        std::cout << "主线程工作中 " << i << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

    return 0;    // 进程退出, 心跳线程一并终止, 不需要 join
}

注:真实程序可以在退出前 running.store(false) 让心跳线程优雅结束;就算忘了置位,进程退出时它也会被一并终止——detach 的线程不会阻止进程退出。

除了"被截断",detach 还有更隐蔽的风险:线程入口若引用/指向了主线程的局部变量,main 返回后这些变量在第 1 步就析构了,后台线程若还在跑,访问它们就是悬空引用(dangling reference),未定义行为:

#include <iostream>
#include <thread>

int main()
{
    int local = 42;
    std::thread t([&local] {          // 捕获了局部变量的引用
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << local << std::endl;   // 危险!main 返回后 local 已销毁
    });
    t.detach();
    return 0;                          // main 立即结束, local 销毁
}

所以 detach 只应在线程完全自给自足(不依赖任何外部对象)时使用。工程上更推荐显式 join 管理线程生命周期,或者用 std::async(见多线程编程进阶)代替裸线程。


2.5 joinable 与线程对象约束

std::thread 对象有可连接(joinable)状态:构造时绑定了线程即为 joinable;已经 join() 或 detach() 后变为非 joinable。规则:

  • join() / detach() 各自只能调用一次,重复调用会抛出 std::system_error
  • 对非 joinable 的对象调用 join() / detach() 同样抛出 std::system_error
  • 析构时如果对象仍然 joinable,会调用 std::terminate 直接终止整个程序(不是抛异常)
#include <iostream>
#include <thread>

int main()
{
    std::thread t([] {});
    t.join();
    t.join();      // t 已不是 joinable, 抛出 std::system_error
    return 0;
}

运行时抛出:

terminate called after throwing an instance of 'std::system_error'
  what():  Invalid argument

std::thread 可移动、不可拷贝:移动后原对象变为非 joinable,新对象接管线程,因而可以通过 joinable() 进行线程对象的安全管理:

#include <iostream>
#include <thread>
#include <vector>

int main()
{
    std::vector<std::thread> pool;

    for(int i = 0; i < 3; ++i)
    {
        std::thread t([i] { std::cout << "线程 " << i << std::endl; });
        pool.push_back(std::move(t));      // 线程不可拷贝, 只能移动进容器
    }

    for(auto& t : pool)
    {
        if(t.joinable())
        {
            t.join();
        }
    }
    return 0;
}

注意:析构时 joinable 会 terminate,所以容器里残留未 join 的线程对象是常见崩溃源。joinable() 检查 + join() 是安全的收尾模式


2.6 线程 ID 与硬件并发数

std::this_thread::get_id() 返回当前线程的 ID(std::thread::id 类型,可用 << 输出);std::thread::hardware_concurrency() 返回硬件支持的并发线程数(逻辑核心数),实现不支持时返回 0:

#include <iostream>
#include <thread>

int main()
{
    std::cout << "当前线程 id: " << std::this_thread::get_id() << std::endl;
    std::cout << "硬件并发数: " << std::thread::hardware_concurrency() << std::endl;
    return 0;
}
当前线程 id: 0x2B
硬件并发数: 24

硬件并发数相关细节

硬件并发数常用来决定线程池的默认规模(如"计算密集任务开 hardware_concurrency() 个线程"),但要留意它返回的是逻辑核心数,包含超线程虚拟出的核心。

以 Core i7‑14650HX 为例,它有 8 个大核(P-core)+ 8 个小核(E-core),大核开启超线程,小核则没有,因而每个大核 2 线程,每个小核 1 线程,总共 24 个逻辑核心。hardware_concurrency() 返回的就是这个数。

如果有些沉重的计算任务跑在小核上,可能会拖慢整体吞吐量;如果有些轻量任务跑在大核上,可能会抢占大核的算力,影响其他线程。C++本身无法区分大核/小核,也无法控制线程绑定到哪个核心,如果要进行更精细的调度,需要用平台特定的 API(Linux 上是 pthread_setaffinity_np,Windows 上是 SetThreadAffinityMask)。

std::this_thread::yield()

std::this_thread::yield() 主动让出当前线程的 CPU 时间片,把执行机会让给同优先级的其他线程。自旋等待(忙等)场景下配合使用可以降低 CPU 占用:

#include <atomic>
#include <iostream>
#include <thread>

int main()
{
    std::atomic<bool> ready{false};

    std::thread worker([&ready] {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        ready.store(true);
    });

    while (!ready.load())     // 忙等, 注意: 这里用原子变量避免数据竞争
    {
        std::this_thread::yield();   // 让出 CPU, 减少空转
    }
    std::cout << "ready!" << std::endl;

    worker.join();
    return 0;
}

注:忙等 + yield 只是演示,生产代码更推荐用条件变量或 std::async 等待(多线程编程进阶 )。原子变量的细节见 原子操作。


2.7 常见错误小结

1. 忘记 join/detach,线程对象析构时 terminate

void foo()
{
    std::thread t([] { /* 长时间任务 */ });
}

int main()
{
    foo();   // t 析构时仍然 joinable, 调用 std::terminate

    std::cout << "main 正常结束" << std::endl;

    return 0;
}

实际情况下,无法执行到 “main 正常结束” 的输出,只能看到:

terminate called without an active exception

2. 局部变量被 detach 的线程访问,悬空引用

int main()
{
    int value = 42;
    std::thread t([&value] { std::cout << value << std::endl; });
    t.detach();     // main 返回后 value 销毁, 子线程访问悬空引用
    return 0;
}

3. 线程入口抛异常且未被捕获:线程入口函数中的异常若没有被捕获,会调用 std::terminate 终止进程。所以线程入口最好整体包在 try-catch 中,或保证内部不会抛出。

void worker()
{
    throw std::runtime_error("boom");
}

int main()
{
    std::thread t(worker);   // worker 抛异常, 线程内无人接住
    t.join();
    return 0;                // 实际上到这里之前进程已经终止
}

4. 不检查 joinable() 就 join:线程可能已经被移动走(容器管理的常见场景),直接 join() 抛 std::system_error。

到这一步,你已经能创建、启动、等待、分离线程了。但真实程序里线程几乎总是要传参数、共享数据、互相通信——这正是系列后续文章的内容。


建议 进一步学习: