• 在C++中使用std::async可能会踩到的几个坑
  • 发布于 1周前
  • 28 热度
    0 评论
陷阱一:默认启动策略——任务啥时候跑,取决于“心情”
问题在哪?
std::async默认的启动策略是 std::launch::async | std::launch::deferred,翻译成人话就是:任务可能立刻在新线程跑,也可能拖到你调用 future.get()或 wait()时才跑,完全看实现的心情。这不扯淡吗?你以为异步任务已经默默干活了,结果它在“摸鱼”,等你催它才动。
错误案例
来看这段代码,你可能觉得很简单,没啥毛病:
#include <future>
#include <iostream>
#include <chrono>

int main() {
    auto future = std::async([] {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << "任务完成啦!" << std::endl;
    });
    std::cout << "主线程结束" << std::endl;
    return 0;
}
你期待啥?主线程打印“主线程结束”,任务在后台睡 2 秒后打印“任务完成啦!”。但现实是:啥也没打印!为啥?因为默认策略可能是 deferred,任务被延迟到 future.get()调用时才跑,而你没调用 get(),future析构时直接把任务扔了。

逐步优化
第一步:诊断问题
加一句 future.get()试试:
int main() {
    auto future = std::async([] {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << "任务完成啦!" << std::endl;
    });
    std::cout << "主线程结束" << std::endl;
    future.get(); // 强制等待任务完成
    return 0;
}
输出:
主线程结束
任务完成啦!
现在任务跑了,但主线程被堵住了 2 秒,这不是异步的初衷啊!
第二步:强制异步
明确告诉 std::async:“别拖延,马上跑!”用 std::launch::async:
int main() {
    auto future = std::async(std::launch::async, [] {
        std::this_thread::sleep_for(std::chrono::seconds(2));
        std::cout << "任务完成啦!" << std::endl;
    });
    std::cout << "主线程结束" << std::endl;
    return 0;
}
输出:
主线程结束
(2秒后)

任务完成啦!


完美!任务立刻在新线程跑,主线程不用等。但别忘了,如果你要用任务结果,还得 future.get(),否则任务跑完就白跑了。
我的建议
默认策略这种“薛定谔的执行”太坑了,建议每次用 std::async都显式指定策略,别让实现自己猜你的意图。

陷阱二:异常处理——不抓异常,程序直接“炸”
问题在哪?
任务里抛异常,std::async会把它塞到 future里。你不调用 get(),异常就藏着;调用 get()不抓异常,程序直接崩。异步任务不能随便崩啊,健壮性咋保证?
错误案例
#include <future>
#include <iostream>

int main() {
    auto future = std::async(std::launch::async, [] {
        throw std::runtime_error("任务出错了!");
    });
    future.get(); // 没抓异常,直接崩
    return 0;
}
运行一下,程序直接挂了,抛出“任务出错了!”。
逐步优化
第一步:加防护
用 try-catch包住 get():
int main() {
    auto future = std::async(std::launch::async, [] {
        throw std::runtime_error("任务出错了!");
    });
    try {
        future.get();
    } catch (const std::exception& e) {
        std::cerr << "捕获异常:" << e.what() << std::endl;
    }
    return 0;
}
输出:
捕获异常:任务出错了!
程序没崩,异常被妥善处理。
我的建议
异步任务的异常处理不是可选项,是必修课。每次 get()都包上 try-catch,别指望任务永远不出错。

陷阱三:资源管理——线程开太多,系统扛不住
问题在哪?
std::async(std::launch::async, ...)每次都可能新建线程,标准没保证用线程池。如果任务一多,线程开到几百上千,系统资源直接被榨干,性能崩盘。
错误案例
#include <future>
#include <vector>

int main() {
    std::vector<std::future<void>> futures;
    for (int i = 0; i < 1000; ++i) {
        futures.push_back(std::async(std::launch::async, [] {
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }));
    }
    for (auto& f : futures) f.get();
    return 0;
}
1000 个线程同时跑,CPU 和内存直接爆炸,小机器可能直接卡死。
逐步优化
第一步:限制线程数
用一个简单线程池代替:
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <vector>

std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;

void worker() {
    while (true) {
        std::function<void()> task;
        {
            std::unique_lock<std::mutex> lock(mtx);
            cv.wait(lock, [] { return !tasks.empty() || stop; });
            if (stop && tasks.empty()) return;
            task = std::move(tasks.front());
            tasks.pop();
        }
        task();
    }
}

int main() {
    std::vector<std::thread> workers;
    for (int i = 0; i < 4; ++i) { // 只用 4 个线程
        workers.emplace_back(worker);
    }

    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        tasks.push([] { std::this_thread::sleep_for(std::chrono::seconds(1)); });
        cv.notify_one();
    }

    {
        std::lock_guard<std::mutex> lock(mtx);
        stop = true;
    }
    cv.notify_all();
    for (auto& w : workers) w.join();
    return0;
}
现在只有 4 个线程循环处理 1000 个任务,资源占用可控。
我的建议
std::async适合小规模任务,大规模并发还是老老实实自己管线程,别让它随便开。

陷阱四:RAII 交互——资源没了,任务还想用
问题在哪?
C++ 的 RAII 是资源管理的灵魂,但 std::async的任务可能在 RAII 对象析构后还跑着,用已释放的资源,直接未定义行为。
错误案例
#include <future>
#include <mutex>

int main() {
    std::future<void> future;
    {
        std::mutex mtx;
        future = std::async(std::launch::async, [&mtx] {
            std::lock_guard<std::mutex> lock(mtx);
        });
    } // mtx 析构
    future.get(); // 任务用已析构的 mtx,炸了
    return 0;
}
mtx没了,任务还想锁它,行为未定义,随时崩。
逐步优化
第一步:延长资源寿命
用 shared_ptr:
int main() {
    auto mtx_ptr = std::make_shared<std::mutex>();
    auto future = std::async(std::launch::async, [mtx_ptr] {
        std::lock_guard<std::mutex> lock(*mtx_ptr);
    });
    future.get(); // mtx_ptr 活到任务结束
    return 0;
}
shared_ptr保证 mutex活到任务结束,安全!

我的建议
异步任务和资源生命周期要“绑一块儿”,别让任务用“死资源”。
总结:std::async好用,但得会用
std::async是多线程的快捷键,但默认策略、异常、资源管理、RAII 这四大陷阱不搞清楚,用起来就是给自己挖坑。我的建议是:用它之前多想一步,策略要明确、异常要抓牢、线程要管住、资源要护好。掌握这些,你的异步代码才能又快又稳,别让多线程变成“拖延症”患者!

用户评论