在 C++11 之后,标准库提供了 std::thread、std::async、std::future 等工具,使多线程编程变得更为便捷。然而,安全使用这些工具仍需注意同步、资源管理与错误传播。本文从三大方面阐述安全使用多线程与 std::async 的关键技巧。
-
避免共享可变状态
- 使用不可变对象:如果任务间不需要共享可变数据,直接将数据拷贝到任务参数中即可。
- 读写分离:读操作多于写操作时,可采用读写锁(
std::shared_mutex)或原子类型(std::atomic)保证并发读安全。 - 避免裸指针:若必须共享指针,使用
std::shared_ptr或std::unique_ptr并配合std::lock_guard或std::unique_lock。
-
正确处理
std::future与异常std::async产生的std::future在get()时会将线程抛出的异常转发到调用者。务必在try/catch语句块中调用get(),否则异常会导致程序崩溃。- 使用
wait()或wait_for()先确认任务完成,避免在不确定状态下get()。 - 对于需要多线程同步的情况,可使用
std::promise+std::future自定义信号量,确保主线程在等待所有子线程完成后才继续。
-
线程池与资源管理
std::async的默认启动方式为async(新线程)或deferred(懒加载)取决于实现。若要统一线程行为,建议显式指定std::launch::async,并配合std::thread::detach()或join()。- 长时间运行的任务最好使用线程池(如
ThreadPool库或自定义实现),可减少线程创建销毁开销。 - 对于可能被
std::async产生的后台线程,应在程序退出前确保其完成。可通过future.get()或future.wait()等方法实现。
-
避免死锁与竞态
- 按固定顺序获取锁,或使用
std::scoped_lock(C++17)一次性获取多个锁。 - 关注
std::future与std::promise的生命周期,避免在对象销毁前未获取结果。 - 对于需要同步的数据结构(如队列),考虑使用
std::condition_variable以阻塞等待,而非忙等待。
- 按固定顺序获取锁,或使用
-
调试与测试
- 使用 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 与多线程的核心在于:
- 避免共享可变状态,或通过原子、锁实现同步;
- 正确处理异常,在
future.get()前使用try/catch; - 资源管理,确保所有线程在程序结束前已完成或已
detach; - 防止死锁,使用一致的锁获取顺序;
- 充分测试,借助工具检测竞争。
只要遵循这些原则,即使在复杂的并发环境中,也能保持代码的安全性与可维护性。