14. Lambda 与多线程:std::thread 和 std::async 中的 Lambda 使用

多线程编程,说白了就是让程序同时干几件事。C++11 之后,Lambda 表达式成了开启线程最顺手的那把钥匙。我个人习惯,只要线程任务不复杂,直接用 Lambda 往里塞,比写个函数对象省事多了。

今天我们就聊聊,怎么把 Lambda 扔进 std::threadstd::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();  // 注意!这里可能有问题
⚠️ 我曾经踩过的坑: 上面这段代码,如果线程 detached 了,而主线程先结束,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();

这里 countermtx 都是引用捕获。我建议你养成习惯:多线程里,只要涉及共享数据,先想锁,再想捕获方式

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 与多线程的核心关系:

Lambda + 多线程 std::thread 值捕获 [=] / 引用捕获 [&] detach 时注意生命周期 需要手动加锁 std::async 返回 future,可获取结果 指定启动策略 支持 deferred 懒加载 C++14 初始化捕获(移动语义)

14.6 避坑指南

最后,我把自己踩过的几个坑列出来,你遇到了直接绕开:

  • 不要捕获局部变量的引用然后 detach —— 线程还没跑完,局部变量就没了。
  • Lambda 里捕获 this 指针时,确保对象还活着 —— 我见过一个同事,对象析构了,线程还在调用成员函数,直接崩溃。
  • 值捕获大对象时注意性能 —— 比如捕获一个 std::vector<int> 有 10 万个元素,拷贝一次就是 40 万字节。用移动语义或者共享指针。
  • std::async 默认策略可能不启动新线程 —— 如果你依赖多线程来提升性能,记得显式指定 std::launch::async

总结一下: Lambda 让多线程代码写起来很爽,但爽的背后是责任。捕获方式、生命周期、同步机制,这三件事想清楚了,线程安全就稳了一大半。


公众号:蓝海资料掘金营,微信deep3321