11、C++11 并发编程:std::thread与std::async,开启多线程之旅。

说实话,在C++11之前,写多线程程序是一件挺痛苦的事。你得用POSIX线程或者Windows API,不同平台的代码还不一样。我记得刚入行那会儿,为了在Linux和Windows上跑同一个多线程模块,硬是写了两个版本的代码,维护起来简直是一场噩梦。

C++11终于把并发编程纳入了标准库。这意味着一件事:你可以用同一套代码,跨平台地创建线程、管理异步任务。今天我们就来聊聊两个最核心的工具——std::threadstd::async

std::thread:最基础的线程控制

std::thread说白了就是直接创建一个操作系统线程。你给它一个可调用对象,它就在新线程里执行。用法非常直观:

#include <iostream>
#include <thread>

void hello() {
    std::cout << "Hello from thread!\n";
}

int main() {
    std::thread t(hello);
    t.join();  // 等待线程结束
    return 0;
}

嗯,这里要注意一个关键点:必须显式调用join()detach()。如果std::thread对象析构时线程还在运行,程序会直接终止。我曾经在项目里见过一个bug,就是因为某个分支路径忘了调用join(),导致程序莫名其妙崩溃。排查了半天才发现是线程对象销毁时抛了异常。

警告: 永远不要让 std::thread 对象在可联结(joinable)状态下析构。要么 join(),要么 detach(),没有第三条路。

给线程传参

线程函数可以带参数,直接跟在可调用对象后面就行:

void print_sum(int a, int b) {
    std::cout << a + b << "\n";
}

int main() {
    std::thread t(print_sum, 3, 4);
    t.join();
    return 0;
}

这里有个坑:参数默认是按值传递的。如果你想传引用,必须用std::ref包装一下。我见过有人直接传引用,结果编译不过,一脸懵地跑来问我。其实标准库这么做是为了安全——避免悬空引用。

void modify(int& x) {
    x = 42;
}

int main() {
    int val = 0;
    std::thread t(modify, std::ref(val));
    t.join();
    // val 现在是 42
    return 0;
}

std::async:更高级的异步任务

我个人习惯,如果只是想让某个函数在后台跑一下,然后拿个结果回来,我更倾向于用std::async。它返回一个std::future对象,你可以通过它获取异步操作的结果。

#include <iostream>
#include <future>

int compute(int n) {
    return n * n;
}

int main() {
    std::future<int> result = std::async(compute, 10);
    std::cout << "Result: " << result.get() << "\n";
    return 0;
}

你想想看,这比手动管理线程方便多了。不用操心join(),不用处理线程生命周期,std::async全帮你搞定了。

启动策略:懒执行还是立即执行?

std::async可以接受一个启动策略参数。默认情况下,实现可以自由选择是立即启动一个新线程,还是延迟到调用get()时才执行。如果你想明确控制,可以用这两个标志:

策略 含义
std::launch::async 强制在新线程上立即执行
std::launch::deferred 延迟执行,直到调用 get()wait()
// 强制异步执行
auto f1 = std::async(std::launch::async, compute, 5);

// 延迟执行
auto f2 = std::async(std::launch::deferred, compute, 6);
int val = f2.get();  // 此时才真正执行
小技巧: 如果你不确定用哪个策略,就用默认的。大多数情况下,标准库的实现会帮你做出合理的选择。只有在性能敏感的场景下,才需要手动指定。

std::thread vs std::async:怎么选?

这个问题我经常被问到。我的建议很简单:

  • 需要精细控制线程生命周期(比如设置线程优先级、绑定CPU核心)→ 用 std::thread
  • 只是想在后台跑个任务,拿个结果 → 用 std::async
  • 需要线程池或大量短任务 → 两个都不太合适,考虑 std::thread_pool(C++20 之后有标准支持)

说白了,std::async是更高层的抽象,隐藏了线程管理的细节。而std::thread给了你更多的控制权,但也意味着你要承担更多的责任。

避坑指南:我曾经踩过的坑

我曾经在项目里用std::async做并行计算,结果发现性能反而比单线程还差。排查了半天,原来是默认启动策略导致任务被延迟执行了,根本没有并行。后来我显式指定了std::launch::async,问题就解决了。

还有一个常见的坑:std::future的析构函数在某些情况下会阻塞等待结果。如果你创建了多个std::future对象,注意它们的析构顺序,避免意外阻塞。

核心要点:
  • std::thread 直接创建线程,需要手动管理生命周期
  • std::async 返回 std::future,自动管理线程
  • 默认启动策略可能延迟执行,性能敏感时显式指定 std::launch::async
  • 传引用参数时用 std::ref

知识结构图

下面这张图帮你理清今天讲的核心内容:

C++11 并发编程核心 std::thread std::async 直接创建操作系统线程 必须 join() 或 detach() 传引用需用 std::ref 返回 std::future 获取结果 自动管理线程生命周期 支持启动策略控制 选 async 省心,选 thread 灵活

好了,今天的内容就到这里。多线程编程是个大话题,但std::threadstd::async是你必须掌握的基础工具。下次遇到需要并行处理的任务,先想想用哪个更合适。


专注资料整理