好的,咱们今天来聊聊C++里那个让人又爱又恨的 std::async
。 这玩意儿吧,说它好用,那是真方便,一句话就能把函数扔到后台执行。 说它坑爹,那也是真坑,一不小心就掉坑里了。
std::async
:异步界的“傻瓜相机”
std::async
就像是异步编程界的“傻瓜相机”,你不用手动创建线程、管理线程生命周期,直接 std::async(你的函数, 参数)
,然后坐等结果就行。 听起来是不是很美好?
基本用法:
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
int calculate_sum(int a, int b) {
std::cout << "开始计算,线程ID: " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
std::cout << "计算结束,线程ID: " << std::this_thread::get_id() << std::endl;
return a + b;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
std::future<int> result = std::async(std::launch::async, calculate_sum, 10, 20); // 强制异步执行
std::cout << "正在等待结果..." << std::endl;
int sum = result.get();
std::cout << "结果是: " << sum << std::endl;
return 0;
}
解释一下:
#include <future>
:std::async
和std::future
都定义在这个头文件里,用之前别忘了加上。std::async(std::launch::async, calculate_sum, 10, 20)
: 这就是关键!std::async
: 启动异步任务。std::launch::async
: 这是一个启动策略,告诉std::async
必须 在新的线程中执行calculate_sum
。 如果不指定,或者使用std::launch::deferred
,或者使用默认的std::launch::any
,std::async
可能不会创建新线程,而是延迟到result.get()
被调用时才执行calculate_sum
。 这就是坑的开始!calculate_sum
: 要异步执行的函数。10, 20
: 传递给calculate_sum
的参数。
std::future<int> result
:std::async
返回一个std::future
对象,它代表了异步操作的结果。 就像一个“期货”,你现在拿到的是一个凭证,将来可以从中获取结果。result.get()
: 阻塞等待异步操作完成,并返回结果。 如果异步操作还没完成,get()
会一直等待。 如果异步操作抛出了异常,get()
也会抛出同样的异常。
启动策略:std::launch::async
vs. std::launch::deferred
vs. std::launch::any
std::async
最让人迷惑的地方就是它的启动策略。 不同的启动策略,行为可是天差地别。
启动策略 | 行为 | 优点 | 缺点 |
---|---|---|---|
std::launch::async |
保证在新的线程中执行。 | 确保异步执行,不会阻塞调用线程。 | 线程创建和销毁有开销。 如果任务很快完成,线程切换的开销可能大于任务本身的执行时间。 |
std::launch::deferred |
延迟执行,直到调用 future.get() 或 future.wait() 时才在 调用线程 中执行。 相当于把函数变成了一个懒加载的函数。 |
没有线程创建和销毁的开销。 适用于只需要偶尔执行的任务,或者任务的执行时间不确定,避免不必要的线程创建。 | 实际上不是异步执行,而是在调用线程中同步执行。 如果你期望的是异步,那这就掉坑里了。 如果 future 对象析构时没有调用 get() 或 wait() ,那么 deferred 的函数将不会被执行。 这会导致潜在的资源泄漏或逻辑错误。 |
std::launch::any |
这是默认策略,让系统自己决定是使用 std::launch::async 还是 std::launch::deferred 。 你没法预测它会怎么做。 |
系统可以根据当前资源情况选择最佳策略,平衡性能和资源利用率。 | 不可预测性! 你不知道你的函数到底是在新线程中执行,还是在调用线程中执行。 这会给调试带来很大的麻烦。 在某些极端情况下,std::launch::any 可能会造成死锁。 比如,如果你的任务需要在调用线程中获取锁,而系统选择了 deferred 策略,那么就会发生死锁。 |
std::future
:异步结果的“保管员”
std::future
是一个模板类,用来存储异步操作的结果。 它可以让你在将来某个时刻获取异步操作的结果,或者检查异步操作是否完成。
std::future
常用方法:
get()
: 阻塞等待异步操作完成,并返回结果。 只能调用一次。 如果再次调用,会抛出异常。wait()
: 阻塞等待异步操作完成,但不返回结果。wait_for(duration)
: 最多等待指定的时长。 如果超时,返回std::future_status::timeout
。valid()
: 检查future
对象是否有效,即是否关联到一个异步操作。share()
: 创建一个std::shared_future
对象,允许多个线程共享同一个结果。
陷阱! 那些年我们踩过的坑
std::async
虽然方便,但一不小心就会掉坑里。
- 忘记
get()
或wait()
#include <iostream>
#include <future>
#include <chrono>
#include <thread>
void do_something() {
std::cout << "开始执行任务..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "任务执行完毕。" << std::endl;
}
int main() {
std::future<void> f = std::async(std::launch::async, do_something);
// 忘记调用 f.get() 或 f.wait()
std::cout << "主线程继续执行..." << std::endl;
return 0;
}
如果你使用了 std::launch::async
,上面的代码会创建一个新的线程来执行 do_something
。 但是,如果 main
函数执行完毕,f
对象被销毁,而 do_something
线程可能还没执行完,那么程序可能会崩溃,或者出现未定义的行为。
- 解决办法: 确保在
future
对象销毁之前调用get()
或wait()
。
std::launch::deferred
的“假异步”
#include <iostream>
#include <future>
int calculate_sum(int a, int b) {
std::cout << "计算线程ID: " << std::this_thread::get_id() << std::endl;
return a + b;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
std::future<int> result = std::async(std::launch::deferred, calculate_sum, 10, 20);
std::cout << "正在等待结果..." << std::endl;
int sum = result.get(); // calculate_sum 在这里才执行,并且是在主线程中执行
std::cout << "结果是: " << sum << std::endl;
return 0;
}
这段代码使用了 std::launch::deferred
,这意味着 calculate_sum
函数不会立即执行,而是在 result.get()
被调用时才在 主线程 中执行。 这根本不是异步!
- 解决办法: 如果你想要真正的异步执行,一定要使用
std::launch::async
。
std::launch::any
的“薛定谔的异步”
#include <iostream>
#include <future>
int calculate_sum(int a, int b) {
std::cout << "计算线程ID: " << std::this_thread::get_id() << std::endl;
return a + b;
}
int main() {
std::cout << "主线程ID: " << std::this_thread::get_id() << std::endl;
std::future<int> result = std::async(calculate_sum, 10, 20); // 默认使用 std::launch::any
std::cout << "正在等待结果..." << std::endl;
int sum = result.get();
std::cout << "结果是: " << sum << std::endl;
return 0;
}
使用了 std::launch::any
,你根本不知道 calculate_sum
是在哪个线程中执行的。 这就像薛定谔的猫,只有在你调用 get()
的时候,你才能知道它到底是不是异步的。
- 解决办法: 避免使用
std::launch::any
! 除非你真的不在乎任务是在哪个线程中执行。 明确指定启动策略,让你的代码行为可预测。
- 异常处理
#include <iostream>
#include <future>
#include <stdexcept>
int calculate_sum(int a, int b) {
if (a < 0 || b < 0) {
throw std::runtime_error("参数不能为负数");
}
return a + b;
}
int main() {
std::future<int> result = std::async(std::launch::async, calculate_sum, -10, 20);
try {
int sum = result.get();
std::cout << "结果是: " << sum << std::endl;
} catch (const std::exception& e) {
std::cerr << "捕获到异常: " << e.what() << std::endl;
}
return 0;
}
如果异步操作抛出了异常,std::future::get()
会重新抛出同样的异常。 你需要使用 try-catch
块来捕获这些异常。
- 解决办法: 始终使用
try-catch
块来处理std::future::get()
可能抛出的异常。
- 死锁
#include <iostream>
#include <future>
#include <mutex>
#include <thread>
std::mutex mtx;
void task1(std::future<int>& f) {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "task1 等待 task2 的结果..." << std::endl;
int result = f.get(); // 等待 task2 完成
std::cout << "task1 收到结果: " << result << std::endl;
}
int task2() {
std::lock_guard<std::mutex> lock(mtx);
std::cout << "task2 开始计算..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "task2 计算完成。" << std::endl;
return 42;
}
int main() {
std::future<int> f = std::async(std::launch::async, task2);
std::thread t1(task1, std::ref(f));
t1.join();
std::cout << "主线程结束。" << std::endl;
return 0;
}
如果std::async
选择了std::launch::deferred
,那么task2
实际上会在task1
调用f.get()
时执行,而task1
已经持有mtx
锁,导致task2
无法获取锁,从而造成死锁。 即使std::async
选择了std::launch::async
,也仍然有可能因为线程调度等原因导致死锁,但概率较低。
- 解决办法:仔细设计你的锁的获取顺序,避免循环依赖。
最佳实践
- 明确指定启动策略: 避免使用
std::launch::any
,明确指定std::launch::async
或std::launch::deferred
,让你的代码行为可预测。 - 始终处理异常: 使用
try-catch
块来捕获std::future::get()
可能抛出的异常。 - 避免死锁: 仔细设计你的锁的获取顺序,避免循环依赖。
- 小心使用
std::launch::deferred
: 确保你真的理解std::launch::deferred
的行为,并且知道它不是真正的异步。 - 尽量使用 RAII 锁: 使用
std::lock_guard
或std::unique_lock
来管理锁,确保锁在离开作用域时自动释放。 - 考虑使用线程池: 如果你需要频繁地创建和销毁线程,可以考虑使用线程池来减少开销。
- 避免在异步任务中访问共享的可变状态: 尽可能使用不可变数据,或者使用适当的同步机制来保护共享的可变状态。
总结
std::async
是一个强大的工具,可以让你轻松地进行异步编程。 但是,它也有一些陷阱,需要小心避开。 记住,明确指定启动策略,始终处理异常,避免死锁,你就能充分利用 std::async
的优势,写出高效、可靠的并发代码。
希望今天的讲解对大家有所帮助! 祝大家编程愉快,少踩坑!