面向已有 C++ 多线程基础的工程实践笔记
重点:任务结果、线程生命周期、
join()、request_stop()、stop_token与协作式取消\[wxWidgets 与 Boost.Asio 线程协作:future_promise 的适用场景与正确用法\]
1. 文档目标
本文整理以下几个容易混淆但非常关键的 C++ 并发概念:
std::promise/std::future的结果传递模型std::async的作用与执行策略std::async返回临时future时为什么会"意外阻塞"- 不调用
future::get()会发生什么 std::thread为什么不能直接带着joinable()状态析构std::terminate()终止的是线程还是整个进程std::jthread相比std::thread改进了什么std::stop_token与传统std::atomic_bool stop的本质区别request_stop()到底做了什么request_stop()后是否一定要立即join()join()的本质是什么- 如何为设备 IO、后台线程、Boost.Asio 等工程代码设计可靠退出流程
本文特别强调一个核心思想:
线程"请求退出"、线程"真正结束"、调用方"确认线程已经结束"是三个不同的动作。
2. 从 promise/future 到 std::async
2.1 promise/future 的基本模型
\[wxWidgets 与 Boost.Asio 线程协作:future_promise 的适用场景与正确用法\]
std::promise<T> 与 std::future<T> 可以理解为:
text
std::promise<T>
= 结果生产者
std::future<T>
= 结果消费者
典型流程:
text
线程 A
│
│ 创建 promise / future
│
├─────────────────────────────┐
│ │
│ 线程 B
│ │
│ 执行任务
│ │
│ promise.set_value()
│ │
│<────────────────────────────┘
│
future.get()
│
▼
取得结果
例如:
cpp
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread worker([p = std::move(p)]() mutable {
p.set_value(123);
});
int value = f.get();
worker.join();
2.2 std::async 是更高层的任务封装
如果任务本身就是:
text
执行一个函数
↓
得到一个结果
那么手工创建:
text
thread
+
promise
+
future
+
异常传递
往往比较繁琐。
std::async 可以直接:
cpp
auto f = std::async(
std::launch::async,
[] {
return 123;
}
);
int result = f.get();
可以把它理解为:
text
std::async()
│
├── 安排 Callable 执行
├── 建立 shared state
├── 保存返回值
├── 保存异常
└── 返回 future
所以:
text
std::thread
更关注"线程"
std::async
更关注"任务 + 结果"
这是从线程级抽象向任务级抽象的提升。
3. std::async 的 shared state
std::async、std::promise、std::future 背后都有一个非常关键的概念:
text
shared state
可以概念化为:
text
shared state
┌─────────────────┐
│ result │
│ exception │
│ ready flag │
│ synchronization │
└────────▲────────┘
│
┌───────────┴───────────┐
│ │
async task future
│ │
return / throw get
例如:
cpp
auto f = std::async(
std::launch::async,
[] {
return 123;
}
);
任务完成后:
text
return 123
↓
shared state
↓
future.get()
↓
123
如果任务抛异常:
text
throw exception
↓
shared state 保存 exception
↓
future.get()
↓
重新抛出 exception
4. std::async 的执行策略
4.1 std::launch::async
cpp
auto f = std::async(
std::launch::async,
work
);
语义:
安排
work在另一个执行线程中运行。
典型时间线:
text
调用线程 async worker
std::async()
│
├──────────────────────────> work()
│ ███████████
│
继续执行其它代码
████████
│
f.get()
│
│ 等待剩余任务
▼
4.2 std::launch::deferred
cpp
auto f = std::async(
std::launch::deferred,
work
);
此时 work() 并不会立即执行。
概念上:
text
std::async()
↓
只保存任务
↓
暂时不执行
↓
future.get() / wait()
↓
此时才执行 work()
因此:
cpp
auto f = std::async(
std::launch::deferred,
work
);
更接近:
text
lazy evaluation
延迟求值
而不是典型的后台并行。
4.3 不指定策略时要谨慎
cpp
auto f = std::async(work);
默认允许实现选择:
text
std::launch::async
或
std::launch::deferred
如果工程上明确要求并行执行,更推荐:
cpp
auto f = std::async(
std::launch::async,
work
);
5. 关键坑点:不保存 std::async 返回的 future
这是本次讨论中非常重要的一个点。
5.1 错误直觉
很多人第一次看到:
cpp
std::async(
std::launch::async,
work
);
do_other_work();
会认为:
text
启动 work()
↓
立即继续
↓
do_other_work()
也就是:
text
work ███████████
do_other_work ███████
但这通常不是实际效果。
5.2 真正发生的事情
std::async() 会返回一个 std::future。
如果没有变量接收:
cpp
std::async(
std::launch::async,
work
);
那么产生的是一个临时 future。
可以概念化成:
cpp
{
std::future<void> temporary =
std::async(
std::launch::async,
work
);
} // 当前 full-expression 结束,temporary 析构
对于 std::launch::async 创建的异步任务,最后一个关联 future 的析构在必要时会等待任务结束。
因此:
text
调用线程 async worker
std::async()
│
├──────────────────────────> work()
│ █████████████
│
临时 future 析构
│
│ 等待..........................
│<──────────────────────────── 完成
│
do_other_work()
████████████
最终变成近似串行:
text
work █████████████
do_other_work ████████
5.3 正确做法
如果希望真正形成并发区间:
cpp
auto f = std::async(
std::launch::async,
work
);
do_other_work();
f.get();
这样:
text
调用线程 async worker
std::async()
│
├──────────────────────────> work()
│ █████████████
│
do_other_work()
██████████
│
f.get()
│
│ 必要时等待
▼
核心结论:
如果希望
std::async(std::launch::async, ...)与调用线程后续代码真正重叠执行,应保存其返回的future。
6. 不调用 future::get() 会发生什么
这是另一个需要明确区分的点。
cpp
auto f = std::async(
std::launch::async,
work
);
// 没有 f.get()
并不表示:
text
future 不 get
↓
worker 被杀掉
不是。
任务仍然会正常运行。
6.1 get() 不是用于"维持线程生命周期"
future::get() 的主要作用是:
text
等待结果 ready
+
取得结果
+
如果任务抛异常则重新抛出
+
使普通 future 失效
如果一直没有 get():
text
async task
↓
正常运行
↓
结果 / 异常进入 shared state
↓
future 最终析构
6.2 不 get() 的主要问题一:结果被丢弃
cpp
auto f = std::async(
std::launch::async,
[] {
return 123;
}
);
如果从不:
cpp
f.get();
则:
text
123
↓
shared state
↓
无人读取
↓
最终销毁
6.3 不 get() 的主要问题二:异常可能被静默丢弃
cpp
auto f = std::async(
std::launch::async,
[] {
throw std::runtime_error("failed");
}
);
异常进入 shared state。
正常应:
cpp
try {
f.get();
}
catch (const std::exception& e) {
// handle error
}
如果从不 get():
text
worker throw
↓
shared state 保存异常
↓
没人读取
↓
future / state 销毁
↓
错误可能被业务逻辑完全忽略
所以:
不
get()通常不是线程生命周期错误,但可能是业务逻辑上的坏味道。
7. std::thread 对象与实际执行线程不是同一个东西
这是理解 join()、detach()、析构行为的基础。
text
std::thread 对象
≠
操作系统里的执行线程
可以把:
cpp
std::thread t(work);
理解成:
text
std::thread t
│
│ 管理 / 关联
▼
OS execution thread
std::thread 更像一个 C++ 层面的线程句柄和生命周期管理对象。
8. 为什么 std::thread 不 join() 会出问题
8.1 std::thread 析构并不会自动结束线程
例如:
cpp
void foo()
{
std::thread t([] {
do_work();
});
}
很多人的直觉是:
text
foo 返回
↓
t 析构
↓
线程一起被销毁
这是错误的。
如果:
cpp
t.joinable() == true
而 t 开始析构:
text
t.~thread()
↓
发现 joinable == true
↓
std::terminate()
9. std::terminate() 终止的是谁
这个点必须非常明确。
text
std::terminate()
不是:
text
终止这个 worker thread
而是:
进入整个 C++ 程序的不可恢复终止流程,通常最终导致整个进程结束。
因此:
text
thread 析构
↓
joinable == true
↓
std::terminate()
↓
整个进程终止
不是:
text
只把 t 对应的线程杀掉
10. 即使线程函数已经执行完,也仍可能需要 join()
这是很容易误解的一点。
cpp
std::thread t([] {
// 很快执行完
});
std::this_thread::sleep_for(
std::chrono::seconds(5)
);
5 秒后 worker 肯定已经执行完。
但是:
cpp
t.joinable();
通常仍然是:
cpp
true
原因:
text
线程函数已经结束
和:
text
std::thread 已完成生命周期收尾
是两回事。
因此仍然需要:
cpp
t.join();
之后:
cpp
t.joinable() == false;
11. join() 的本质
这是本次讨论中另一个核心问题。
一句话:
join()的本质是:等待目标线程结束,并完成线程关联状态的收尾。
它不是:
text
请求线程退出
杀死线程
暂停线程
11.1 第一层作用:等待线程结束
cpp
std::thread t([] {
do_work();
});
t.join();
do_next();
执行:
text
当前线程 worker
创建 t
│
├──────────────────────────> do_work()
│ █████████
│
t.join()
│
│ 等待..........................
│<──────────────────────────── return
│
join 返回
│
do_next()
因此:
cpp
t.join();
返回以后可以确定:
目标线程已经结束。
11.2 第二层作用:解除 std::thread 与执行线程的关联
创建:
cpp
std::thread t(work);
t.joinable(); // true
执行:
cpp
t.join();
之后:
cpp
t.joinable(); // false
即:
text
join 前
std::thread
│
▼
execution thread
joinable = true
join 后
std::thread
不再关联执行线程
joinable = false
11.3 第三层作用:建立线程同步关系
例如:
cpp
int result = 0;
std::thread t([&] {
result = 123;
});
t.join();
std::cout << result;
这是安全的。
因为线程结束与成功 join() 返回之间建立了同步关系。
可以理解为:
text
worker
result = 123
↓
线程结束
↓
happens-before
↓
join() 返回
↓
main 读取 result
因此 join() 不只是"等一下"。
它还是一个明确的:
text
线程同步点
12. detach() 是什么
cpp
std::thread t(work);
t.detach();
含义:
std::thread对象放弃继续管理这个执行线程。
之后:
cpp
t.joinable() == false;
但真正的线程可能继续运行:
text
调用线程 detached worker
thread(...)
│
├──────────────────────────> work()
│ █████████████
│
detach()
│
foo 返回
█████████████
继续执行
注意:
detach()不是结束线程,而是放弃管理线程。
12.1 detach() 的典型风险:悬空引用
cpp
void foo()
{
int value = 123;
std::thread t([&] {
std::this_thread::sleep_for(
std::chrono::seconds(1)
);
std::cout << value;
});
t.detach();
}
foo() 返回后:
text
value 销毁
但 detached worker 之后仍访问:
cpp
value
造成悬空引用与 Undefined Behavior。
因此工程代码中通常不建议随意 detach()。
13. std::jthread:C++20 对线程生命周期的改进
std::jthread 可以看成:
text
joining thread
它相比 std::thread 有两个重要改进:
text
1. 析构时自动处理 join
2. 支持标准化的协作式停止
例如:
cpp
void worker(std::stop_token st)
{
while (!st.stop_requested()) {
do_work();
}
}
void foo()
{
std::jthread t(worker);
}
当 t 析构时,如果仍然 joinable(),大致行为是:
cpp
request_stop();
join();
14. std::stop_token 的核心:不是"杀线程"
std::stop_token 最重要的理解是:
它不是线程终止机制,而是停止请求的观察机制。
例如:
cpp
void worker(std::stop_token st)
{
while (!st.stop_requested()) {
do_work();
}
cleanup();
}
控制线程:
cpp
t.request_stop();
实际发生的是:
text
request_stop()
↓
shared stop-state 标记为
stop requested = true
↓
worker 下次检查
st.stop_requested()
↓
得到 true
↓
worker 自己 cleanup
↓
return
↓
线程真正结束
所以:
text
request_stop()
≠
线程已经停止
而只是:
text
已经发出"请退出"的请求
15. 协作式取消 Cooperative Cancellation
C++20 采用的核心理念是:
text
控制方:
"请你退出。"
worker:
"收到,我在安全位置完成清理后退出。"
而不是:
text
控制方:
"现在立刻把线程杀掉。"
这是为了避免线程在任意位置被终止。
例如线程可能正处于:
cpp
mutex.lock();
update_shared_state();
// 如果此处被强杀
mutex.unlock();
强杀可能造成:
text
mutex 永远不释放
对象析构不执行
文件不关闭
设备状态只更新了一半
数据结构处于不一致状态
因此 C++ 更强调:
text
可控
可清理
可预测
的退出过程。
16. std::stop_token 与 std::atomic_bool stop
C++20 之前常见:
cpp
std::atomic_bool stop = false;
std::thread t([&] {
while (!stop.load()) {
work();
}
});
stop.store(true);
t.join();
C++20:
cpp
std::jthread t([](std::stop_token st) {
while (!st.stop_requested()) {
work();
}
});
t.request_stop();
t.join();
表面逻辑相似:
text
atomic_bool
store(true)
↓
load()
↓
退出
对应:
text
stop_token
request_stop()
↓
stop_requested()
↓
退出
但 stop_token 不只是一个布尔变量。
17. stop_source、stop_token、stop_callback
C++20 把"停止协议"拆成三个角色:
text
shared stop state
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
stop_source stop_token stop_callback
请求停止 查询状态 停止时执行动作
17.1 std::stop_source
代表:
有权发出停止请求的一方。
cpp
std::stop_source source;
source.request_stop();
17.2 std::stop_token
代表:
只观察停止状态的一方。
cpp
std::stop_token token =
source.get_token();
if (token.stop_requested()) {
return;
}
stop_token 本身不能:
cpp
request_stop();
这实现了权限语义:
text
source
= 控制权
token
= 观察权
比传:
cpp
std::atomic_bool&
更清晰。
17.3 std::stop_callback
可以在停止被请求时执行一个动作。
例如:
cpp
void worker(std::stop_token st)
{
std::stop_callback cb(st, [&] {
device.cancel_read();
});
device.blocking_read();
}
当:
cpp
t.request_stop();
发生:
text
request_stop()
↓
stop-state = requested
↓
stop_callback
↓
device.cancel_read()
↓
blocking_read() 被唤醒
↓
worker 可以继续退出
这对于 IO 线程非常重要。
18. request_stop() 可以主动调用,不只在析构时调用
这是本次单独确认的一个重要点。
完全可以:
cpp
std::jthread t(worker);
// ...
t.request_stop();
由开发者根据业务时机主动决定。
例如:
text
用户点击 Stop
设备断开
应用准备退出
任务被取消
测量流程终止
模式切换
都可以主动:
cpp
worker_.request_stop();
18.1 jthread 析构只是最终兜底
可以主动:
cpp
t.request_stop();
t.join();
也可以:
cpp
t.request_stop();
// 暂时不 join
最终:
text
~jthread()
↓
request_stop()
↓
join()
因此析构行为是:
RAII 最终兜底。
而不是唯一的停止入口。
19. request_stop() 后是否一定要立即 join()
答案:
不一定。
完全可以:
cpp
t.request_stop();
do_something_else();
// 不手工 join
最后由:
cpp
~jthread()
完成 join()。
19.1 什么时候可以不立即 join()
如果你只希望:
text
"你开始退出吧"
那么:
cpp
t.request_stop();
即可。
调用线程可以继续做其它事情:
cpp
t.request_stop();
save_ui_state();
write_log();
cleanup_other_resources();
这样 worker 与调用线程甚至可以并行清理。
19.2 什么时候必须 join()
如果后续逻辑要求:
从这一行开始,我必须保证 worker 已彻底结束。
那么必须:
cpp
t.request_stop();
t.join();
例如:
cpp
worker_.request_stop();
worker_.join();
device_.close();
因为:
cpp
request_stop();
返回时 worker 可能仍然正在:
cpp
device_.read();
20. request_stop() 与 join() 的职责完全不同
建议直接记住:
text
request_stop()
=
"请开始退出"
join()
=
"我在这里等到你真正退出"
完整生命周期:
text
request_stop()
↓
停止请求发出
↓
worker 检查 stop_token
↓
退出主循环
↓
cleanup
↓
return
↓
执行线程真正结束
↓
join()
↓
调用方确认退出完成
所以:
cpp
t.request_stop();
t.join();
不是重复操作。
它们分别对应:
text
控制请求
+
完成确认
21. request_stop() 不保证阻塞操作立即结束
这是使用 stop_token 时最容易犯的错误之一。
例如:
cpp
void worker(std::stop_token st)
{
while (!st.stop_requested()) {
blocking_read();
}
}
如果 worker 卡在:
cpp
blocking_read();
即使另外一个线程:
cpp
t.request_stop();
也只是:
text
stop-state = requested
worker 并没有机会执行:
cpp
st.stop_requested();
因此仍可能一直阻塞。
22. sleep_for() 也不会自动响应 stop_token
例如:
cpp
void worker(std::stop_token st)
{
while (!st.stop_requested()) {
std::this_thread::sleep_for(
std::chrono::seconds(30)
);
}
}
如果刚进入 30 秒 sleep 就:
cpp
t.request_stop();
线程不会自动立即醒来。
可能:
text
0s sleep_for(30s)
1s request_stop()
↓
仍在 sleep
30s sleep 结束
↓
检查 stop_requested()
↓
return
所以:
stop_token是停止状态通知,不是系统级线程 interrupt。
23. 一个可靠的线程退出设计:三阶段
对于设备控制、采集线程、后台服务,推荐把退出设计成三个阶段:
text
① request stop
↓
② break blocking operation
↓
③ cleanup + return
例如:
cpp
void DeviceWorker::run(std::stop_token st)
{
std::stop_callback cancel(st, [this] {
port_.cancel();
});
while (!st.stop_requested()) {
auto command = wait_command();
if (st.stop_requested()) {
break;
}
execute(command);
}
stop_motion_if_needed();
flush_pending_commands();
close_device();
}
外部:
cpp
worker_.request_stop();
worker_.join();
对应语义:
text
request_stop()
↓
不要再接受新工作,准备退出
port_.cancel()
↓
把阻塞中的 worker 唤醒
cleanup
↓
让设备恢复安全状态
return
↓
线程真正结束
join()
↓
调用方确认退出完成
24. jthread 的析构顺序与成员生命周期
假设:
cpp
class DeviceController
{
Device device_;
std::jthread worker_;
};
C++ 成员按声明顺序的逆序析构:
text
worker_ 先析构
↓
request_stop + join
↓
device_ 后析构
这是比较安全的。
但如果写成:
cpp
class DeviceController
{
std::jthread worker_;
Device device_;
};
析构顺序:
text
device_ 先析构
↓
worker_ 后析构
如果 worker 仍访问:
cpp
device_
就可能出现生命周期问题。
工程上如果线程依赖多个成员资源,建议显式控制退出:
cpp
DeviceController::~DeviceController()
{
worker_.request_stop();
if (worker_.joinable()) {
worker_.join();
}
// 从这里开始可以确信:
// worker 不再访问任何成员
}
25. std::thread、std::jthread、std::async 的生命周期对比
| 工具 | 核心关注点 | 析构时仍有任务 | 停止机制 |
|---|---|---|---|
std::thread |
线程 | 若仍 joinable() → std::terminate() |
自己设计 |
std::jthread |
RAII 线程 | request_stop() + join() |
stop_token |
std::async |
异步任务 + 结果 | async 关联 future 最终析构可能等待 | 无标准主动取消 |
std::future |
异步结果 | 管理 shared state | 不负责取消任务 |
26. join()、request_stop()、get() 不要混为一谈
request_stop()
text
作用:
发出"请退出"的请求
不保证:
线程已经退出
join()
text
作用:
等待线程真正结束
完成线程关联状态收尾
建立同步关系
不负责:
请求线程退出
future::get()
text
作用:
等待异步结果 ready
取得返回值
重新抛出任务异常
不负责:
强制停止任务
27. 一个非常重要的统一心智模型
27.1 对 jthread
text
控制线程
│
request_stop()
│
▼
shared stop-state
│
├──────────────► stop_token
│ │
│ stop_requested()
│ │
│ ▼
│ worker
│ │
│ cleanup
│ │
│ return
│ │
▼ ▼
join() <────────── worker 已结束
│
▼
继续执行
27.2 对 async
text
std::async()
│
├──────────────► async task
│ │
│ return
│ │
│ ▼
│ shared state
│ │
▼ ▼
std::future <──────── result / exception
│
▼
get()
28. 在 wxWidgets + Boost.Asio 工程中的建议
对于桌面工业软件,可以继续维持:
text
wxWidgets UI thread
│
│ asio::post
▼
Boost.Asio worker / strand
│
│ device IO
▼
得到结果
│
│ CallAfter / wx event
▼
wxWidgets UI thread
不要因为学了 std::async 就把 UI 改成:
cpp
auto f = std::async(
std::launch::async,
read_device
);
auto result = f.get(); // UI thread blocking
因为:
text
async 解决"任务在哪里执行"
但:
text
future.get()
仍然会阻塞调用线程
因此 UI 线程仍然推荐:
text
callback
event
CallAfter
wxQueueEvent
asio completion handler
29. std::async 适合什么场景
比较适合:
text
一次性 CPU 计算
少量粗粒度并行任务
后台计算并最终得到 Result
测试代码等待一次异步结果
临时并行化同步函数
fork → compute → join 模型
例如:
cpp
auto fa = std::async(
std::launch::async,
compute_a
);
auto fb = std::async(
std::launch::async,
compute_b
);
auto a = fa.get();
auto b = fb.get();
merge(a, b);
30. std::async 不适合什么场景
通常不适合:
text
大量小任务
线程池
长期设备 IO loop
串口持续接收
相机 acquisition loop
持续事件流
高频状态刷新
复杂任务取消
精细线程 affinity / priority 控制
这些更适合:
text
Boost.Asio
thread pool
executor
callback
message queue
event queue
observer
专用 worker thread
31. 工程实践建议
31.1 使用 std::async 时
如果希望并行:
cpp
auto f = std::async(
std::launch::async,
work
);
不要随手:
cpp
std::async(
std::launch::async,
work
);
否则临时 future 可能让当前语句结束时直接等待。
31.2 使用 std::thread 时
必须明确:
cpp
t.join();
或:
cpp
t.detach();
不要让一个:
cpp
joinable() == true
的 std::thread 直接析构。
31.3 C++20 优先考虑 std::jthread
如果任务天然具有:
text
启动
运行
请求停止
清理
退出
生命周期,可以优先:
cpp
std::jthread
+
std::stop_token
而不是自己维护:
cpp
std::thread
+
std::atomic_bool stop_
+
手写析构 join
31.4 不要把 request_stop() 当作线程已经结束
错误:
cpp
worker_.request_stop();
device_.destroy(); // 认为 worker 已不再访问 device
正确:
cpp
worker_.request_stop();
worker_.join();
device_.destroy();
如果资源销毁要求 worker 已彻底结束,就必须等待。
32. 最终记忆版
std::async
text
我要执行一个任务并得到结果
std::future
text
我要观察 / 等待 / 取得异步结果
std::thread
text
我要直接管理一个执行线程
std::jthread
text
我要一个具备 RAII join
并支持协作式停止的线程
request_stop()
text
"请开始退出"
stop_token
text
"有人请求我退出了吗?"
stop_callback
text
"收到退出请求时,顺便执行取消 IO / 唤醒等待等动作"
join()
text
"我在这里等到线程真正结束,
并完成线程生命周期收尾"
future::get()
text
"我在这里等结果,
取回结果或重新抛出异步任务异常"
33. 最关键的四个结论
结论 1
text
request_stop()
≠
线程已经停止
它只是提出停止请求。
结论 2
text
join()
≠
让线程退出
它负责等待并确认线程已经结束。
结论 3
text
std::thread 析构时仍 joinable
↓
std::terminate()
↓
整个进程终止
不是只结束那个 worker thread。
结论 4
text
std::async(std::launch::async, work);
如果不保存返回的 future:
text
临时 future
↓
当前语句结束时析构
↓
必要时等待 async task
↓
调用线程无法继续执行后续代码
因此可能失去原本想要的并行效果。
34. 一句话总结整套体系
std::thread管线程,std::async管任务结果,std::jthread管具备 RAII 与协作式退出的线程;request_stop()发出退出意图,worker 自己安全退出,而join()负责等待并确认退出已经完成。