14. Lambda 与多线程:std::thread 和 std::async 中的 Lambda 使用
多线程编程,说白了就是让程序同时干几件事。C++11 之后,Lambda 表达式成了开启线程最顺手的那把钥匙。我个人习惯,只要线程任务不复杂,直接用 Lambda 往里塞,比写个函数对象省事多了。
今天我们就聊聊,怎么把 Lambda 扔进 std::thread 和 std::async 里,以及那些容易踩的坑。
14.1 把 Lambda 扔进 std::thread
先看最基础的用法。你直接传一个 Lambda 给 std::thread 的构造函数,它就开跑了。
#include <iostream>
#include <thread>
int main() {
std::thread t([]{
std::cout << "Hello from thread!" << std::endl;
});
t.join();
return 0;
}
嗯,就这么简单。但实际项目里,你往往需要传参数。Lambda 的捕获列表这时候就派上用场了。
int id = 42;
std::string msg = "Processing";
std::thread t([id, &msg]{
std::cout << "ID: " << id << ", Msg: " << msg << std::endl;
});
t.detach(); // 注意!这里可能有问题
msg 这个引用就悬空了。线程访问一个已经被销毁的变量,轻则乱码,重则崩溃。所以,detach 的线程里尽量用值捕获,或者确保引用对象的生命周期比线程长。
14.2 捕获方式的选择:值 vs 引用
这里我直接给结论:
| 捕获方式 | 适用场景 | 风险 |
|---|---|---|
[=] 值捕获 |
线程生命周期不确定,或需要拷贝数据 | 大对象拷贝开销大 |
[&] 引用捕获 |
线程生命周期可控,且需要修改外部变量 | 悬空引用、数据竞争 |
[this] 捕获 this |
在成员函数中启动线程 | 对象被销毁后线程还在跑 |
你想想看,如果线程里要修改一个外部变量,用引用捕获是必须的。但别忘了加锁。
int counter = 0;
std::mutex mtx;
std::thread t([&counter, &mtx]{
for (int i = 0; i < 1000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
});
t.join();
这里 counter 和 mtx 都是引用捕获。我建议你养成习惯:多线程里,只要涉及共享数据,先想锁,再想捕获方式。
14.3 用 std::async 获取返回值
std::thread 有个麻烦——它拿不到线程的返回值。你得自己搞个共享变量,或者用 std::promise。但 std::async 就优雅多了,它返回一个 std::future,Lambda 的返回值直接通过 future.get() 拿到。
#include <iostream>
#include <future>
int main() {
auto fut = std::async(std::launch::async, [](int a, int b) -> int {
return a + b;
}, 3, 4);
std::cout << "Result: " << fut.get() << std::endl; // 输出 7
return 0;
}
这里有个细节:std::launch::async 强制在新线程上执行。如果不指定,默认是 std::launch::deferred | std::launch::async,也就是由系统决定是立即执行还是延迟执行。我个人习惯,明确指定启动策略,避免意外。
std::launch::deferred,Lambda 会在 future.get() 调用时才执行,而且是同步的。这可以用来实现懒加载。
14.4 捕获移动语义的对象
C++14 之后,Lambda 支持初始化捕获,也就是移动捕获。这对多线程特别有用,比如你要把一个 std::unique_ptr 传给线程。
auto ptr = std::make_unique<int>(42);
std::thread t([p = std::move(ptr)]{
std::cout << *p << std::endl;
});
t.join();
注意,ptr 被移动后,原对象就空了。这是所有权转移,线程拥有了这个指针。我在项目中处理大块内存时,经常用这种方式,避免拷贝开销。
14.5 知识结构图
下面这张图,帮你理清 Lambda 与多线程的核心关系:
14.6 避坑指南
最后,我把自己踩过的几个坑列出来,你遇到了直接绕开:
- 不要捕获局部变量的引用然后 detach —— 线程还没跑完,局部变量就没了。
- Lambda 里捕获 this 指针时,确保对象还活着 —— 我见过一个同事,对象析构了,线程还在调用成员函数,直接崩溃。
- 值捕获大对象时注意性能 —— 比如捕获一个
std::vector<int>有 10 万个元素,拷贝一次就是 40 万字节。用移动语义或者共享指针。 - std::async 默认策略可能不启动新线程 —— 如果你依赖多线程来提升性能,记得显式指定
std::launch::async。
总结一下: Lambda 让多线程代码写起来很爽,但爽的背后是责任。捕获方式、生命周期、同步机制,这三件事想清楚了,线程安全就稳了一大半。
公众号:蓝海资料掘金营,微信deep3321