C++ 中如何安全地使用多线程与 std::async?

在 C++11 之后,标准库提供了 std::threadstd::asyncstd::future 等工具,使多线程编程变得更为便捷。然而,安全使用这些工具仍需注意同步、资源管理与错误传播。本文从三大方面阐述安全使用多线程与 std::async 的关键技巧。

  1. 避免共享可变状态

    • 使用不可变对象:如果任务间不需要共享可变数据,直接将数据拷贝到任务参数中即可。
    • 读写分离:读操作多于写操作时,可采用读写锁(std::shared_mutex)或原子类型(std::atomic)保证并发读安全。
    • 避免裸指针:若必须共享指针,使用 std::shared_ptrstd::unique_ptr 并配合 std::lock_guardstd::unique_lock
  2. 正确处理 std::future 与异常

    • std::async 产生的 std::futureget() 时会将线程抛出的异常转发到调用者。务必在 try/catch 语句块中调用 get(),否则异常会导致程序崩溃。
    • 使用 wait()wait_for() 先确认任务完成,避免在不确定状态下 get()
    • 对于需要多线程同步的情况,可使用 std::promise + std::future 自定义信号量,确保主线程在等待所有子线程完成后才继续。
  3. 线程池与资源管理

    • std::async 的默认启动方式为 async(新线程)或 deferred(懒加载)取决于实现。若要统一线程行为,建议显式指定 std::launch::async,并配合 std::thread::detach()join()
    • 长时间运行的任务最好使用线程池(如 ThreadPool 库或自定义实现),可减少线程创建销毁开销。
    • 对于可能被 std::async 产生的后台线程,应在程序退出前确保其完成。可通过 future.get()future.wait() 等方法实现。
  4. 避免死锁与竞态

    • 按固定顺序获取锁,或使用 std::scoped_lock(C++17)一次性获取多个锁。
    • 关注 std::futurestd::promise 的生命周期,避免在对象销毁前未获取结果。
    • 对于需要同步的数据结构(如队列),考虑使用 std::condition_variable 以阻塞等待,而非忙等待。
  5. 调试与测试

    • 使用 ThreadSanitizer 或 AddressSanitizer 检测数据竞争。
    • 设计单元测试时,应覆盖多线程路径,如并发读写、异常传播、资源释放等。
    • 对性能敏感的代码,可使用 std::chrono::high_resolution_clock 记录耗时,找出瓶颈。

示例代码(C++17):

#include <iostream>
#include <future>
#include <vector>
#include <chrono>
#include <mutex>

std::mutex io_mutex;

void worker(int id, std::promise <int> result)
{
    try {
        // 模拟计算
        std::this_thread::sleep_for(std::chrono::milliseconds(100 + id*10));
        int value = id * id;

        // 把结果传给 promise
        result.set_value(value);

        // 安全打印
        std::lock_guard<std::mutex> lock(io_mutex);
        std::cout << "Worker " << id << " finished.\n";
    } catch (...) {
        // 捕获异常并传给 promise
        result.set_exception(std::current_exception());
    }
}

int main()
{
    const int n = 5;
    std::vector<std::future<int>> futures;
    std::vector<std::promise<int>> promises(n);

    // 启动多线程任务
    for (int i = 0; i < n; ++i) {
        futures.push_back(promises[i].get_future());
        std::async(std::launch::async, worker, i, std::move(promises[i]));
    }

    // 等待结果并处理异常
    for (int i = 0; i < n; ++i) {
        try {
            int res = futures[i].get(); // 若子线程抛异常,这里会传播
            std::cout << "Result from worker " << i << ": " << res << '\n';
        } catch (const std::exception& e) {
            std::cerr << "Worker " << i << " error: " << e.what() << '\n';
        }
    }
}

总结
安全使用 std::async 与多线程的核心在于:

  1. 避免共享可变状态,或通过原子、锁实现同步;
  2. 正确处理异常,在 future.get() 前使用 try/catch
  3. 资源管理,确保所有线程在程序结束前已完成或已 detach
  4. 防止死锁,使用一致的锁获取顺序;
  5. 充分测试,借助工具检测竞争。

只要遵循这些原则,即使在复杂的并发环境中,也能保持代码的安全性与可维护性。

发表评论