线程是进程内的一条执行路径。一个进程可以有多个线程;这些线程共享进程中的地址空间和许多资源,同时各自具有执行上下文和调用栈。多进程也可以是“每个进程都包含多个线程”,不能把二者当作互斥选择。
本页以 **C++17 的 std::thread**为主,重点是启动参数、对象寿命和结束时谁负责等待。同步方法另见互斥量、条件变量和线程池。
图是接口导航,不代表这些机制可以相互替代。例如线程负责执行,互斥量保护共享状态,future传递异步结果;仅仅“创建了线程”并没有完成同步。
1. 进程、线程与代价
跳转到“1. 进程、线程与代价”| 维度 | 多线程 | 多进程 |
|---|---|---|
| 内存关系 | 通常共享进程内对象,可直接传递有效地址,但必须处理同步和寿命。 | 通常有独立地址空间,需IPC或显式共享内存。 |
| 故障边界 | 某个线程造成的内存破坏可影响整个进程。 | 操作系统通常提供更强隔离,但共享资源故障仍可传播。 |
| 开销 | 创建、调度、栈和锁竞争都有代价。 | 创建、地址空间及通信通常更重,实际结果取决于系统和任务。 |
| 适用例子 | 同一应用内的并行计算、后台I/O和响应分离。 | 独立服务、隔离不可信组件、独立重启。 |
“并发”表示多个任务在一段时间内推进,不保证同时执行;“并行”才涉及同时使用执行资源。线程数多于可用CPU核心仍可能有意义,例如等待I/O,但也可能增加上下文切换和竞争。
2. 构造时传函数,不要先调用它
跳转到“2. 构造时传函数,不要先调用它”std::thread worker(function, argument)在新线程中调用函数。写成 std::thread worker(function(argument))会先在当前线程求值;如果函数返回 void,这个表达式根本无法构造线程。
完整例子如下:
#include <cassert>#include <functional>#include <thread>
void add(int& result, int amount){ result += amount;}
int main(){ int result = 0; std::thread worker(add, std::ref(result), 100); assert(worker.joinable()); worker.join(); assert(!worker.joinable()); assert(result == 100);}默认情况下,构造线程时会复制或移动保存传入的函数和参数;要传引用,使用 std::ref并保证被引用对象活得足够久。这里 result一直存在,而且主线程等待完成后才读取它,没有与工作线程同时读写。传指针并不自动复制它指向的对象。
可调用对象可以是普通函数、lambda、带 operator()的函数对象或成员函数与对象组合。成员函数示意为 std::thread(&Worker::run, &object, argument),其 object也必须活到调用结束。
3. joinable描述关联,不描述是否仍在运行
跳转到“3. joinable描述关联,不描述是否仍在运行”| 操作/状态 | 意义 |
|---|---|
默认构造的 std::thread | 没有关联线程,joinable()为false。 |
| 启动后,尚未join/detach | 有关联线程;即使函数已经返回,也仍可为joinable。 |
join() | 等待关联线程结束,完成后解除关联;同时为观察其结果提供同步。 |
detach() | 解除对象与执行线程的关联,之后无法通过此对象join它。 |
| 析构仍joinable的thread对象 | 调用 std::terminate(),不是自动join或detach。 |
不能对没有关联线程的对象调用 join(),也不能让线程join自己。joinable()只是调用前的一项状态检查,不让并发操作同一个thread对象自动变得安全。C++工作草案:thread成员、析构
std::thread不可复制,可以移动。移动转移管理关联:移动后的源对象不再joinable;把新thread移动赋值到一个仍joinable的目标同样会终止程序,应先处理原关联。
4. 用RAII覆盖异常路径
跳转到“4. 用RAII覆盖异常路径”正常路径末尾写一个 join(),仍可能被中间抛出的异常跳过。下面的有限例子用拥有线程的守卫,在离开作用域时等待。任务所引用的 result声明在守卫之前,因此其寿命覆盖守卫析构中的等待。
#include <cassert>#include <stdexcept>#include <thread>#include <utility>
class JoiningThread {public: explicit JoiningThread(std::thread thread) : thread_(std::move(thread)) {} JoiningThread(const JoiningThread&) = delete; JoiningThread& operator=(const JoiningThread&) = delete; ~JoiningThread() { if (thread_.joinable()) thread_.join(); }private: std::thread thread_;};
int main(){ int result = 0; try { JoiningThread worker(std::thread([&result] { result = 42; })); throw std::runtime_error("main-thread operation failed"); } catch (const std::runtime_error&) { assert(result == 42); }}守卫必须由其他线程持有,不能在其管理的线程中销毁自身。它保证等待,不会让无限循环自动停止;如果工作线程等待一个永远不会再被满足的条件,析构也会一直等。建立停止标志、通知和退出条件是业务协议的一部分。
工作线程内抛出的异常不会自动传到创建者。让异常逃出线程入口会终止程序;应在线程入口内捕获并保存,或使用 std::promise/std::future、std::packaged_task等传递失败。C++20的 std::jthread提供自动join和协作停止接口,但也不会强行打断不配合停止的任务。
5. 为什么不默认detach
跳转到“5. 为什么不默认detach”detach()之后,线程仍然需要有效的数据和可预测的结束机制。如果后台lambda捕获局部变量引用、栈对象地址或已经销毁的 this,就可能访问悬空对象。把thread对象分离并没有延长这些对象的寿命。
主线程从 main返回也不会自动等待所有已分离线程完成业务。需要结果、错误处理或正常退出顺序时,优先保留所有权并join,或交给明确拥有任务的线程池。只有真正独立、寿命有完整设计的后台任务才考虑分离。
6. 标识符、等待与调度提示
跳转到“6. 标识符、等待与调度提示”std::this_thread是命名空间,包含当前线程相关函数;不是可以构造的类。
| 接口 | 使用边界 |
|---|---|
thread.get_id() | 取得该对象关联线程的标识;无关联时为默认构造的 thread::id。 |
this_thread::get_id() | 取得当前执行线程标识,可做相等比较和日志。 |
this_thread::yield() | 调度提示,不保证另一指定线程马上运行。 |
this_thread::sleep_for(duration) | 等待一段时间,实际恢复可能更晚;不提供共享数据同步。 |
this_thread::sleep_until(timePoint) | 等待目标时刻,时钟种类与调整会影响语义。 |
thread::hardware_concurrency() | 可用硬件并发度的提示,可能返回0,不是最佳线程池大小保证。 |
线程ID不是“主线程等于1、后续线程按2、3编号”的标准保证,也不能依赖具体打印格式或线程结束后永不复用。
如果需求是“下一整分钟”,先明确使用墙钟;如果是“从现在起经过60秒”,通常使用单调的 steady_clock。下面只计算目标时间,避免示例运行时让读者等待一分钟:
#include <cassert>#include <chrono>
int main(){ using namespace std::chrono; const auto now = system_clock::now(); const auto nextMinute = floor<minutes>(now) + minutes(1); assert(nextMinute > now && nextMinute - now <= minutes(1)); // 若业务需要等待,可传给 std::this_thread::sleep_until(nextMinute)。}system_clock可能随校时变化;睡到整分钟也不保证在那个瞬间获得CPU。定时精度、截止时间和线程同步是三个不同问题,不能用随手增加 sleep_for替代正确的条件变量或互斥保护。