函数式并发:std::async、std::future、Lambda与异步任务
并发编程,说白了就是让程序同时干多件事。传统C++里,你得手动创建线程、管理锁、处理条件变量……写起来又臭又长。但有了Lambda和std::async,事情就简单多了。
我个人习惯把这种模式叫做「扔出去,等结果」。你定义一个任务,把它扔给后台线程,然后该干嘛干嘛。等需要结果了,再伸手去拿。整个过程就像点外卖——你下单,然后继续工作,外卖到了再吃。
std::async:最简单的异步启动方式
std::async是C++11引入的工具。它接受一个可调用对象(Lambda、函数指针、仿函数都行),然后异步执行它。返回值是一个std::future对象,用来获取结果。
看个最简单的例子:
#include <iostream>
#include <future>
#include <thread>
int main() {
// 异步执行一个Lambda
std::future<int> result = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
});
std::cout << "正在等待结果..." << std::endl;
// 获取结果(会阻塞直到任务完成)
int value = result.get();
std::cout << "结果是: " << value << std::endl;
return 0;
}
这里有个细节:std::launch::async表示强制在新线程上执行。还有另一个策略std::launch::deferred,它不会创建新线程,而是等到调用get()时才在当前线程执行。嗯,这个要区分清楚。
std::future::get()只能调用一次。第二次调用会抛出std::future_error异常。如果你需要多次获取结果,考虑用std::shared_future。
std::future:异步结果的占位符
std::future本质上是一个「未来才会有的值」。它提供了几个关键操作:
get():阻塞等待结果,并返回wait():只等待,不获取结果wait_for():等待一段时间,超时返回状态wait_until():等待到某个时间点
我在项目中遇到过一个问题:用wait_for轮询多个future时,如果超时时间设得太短,CPU占用会飙升。后来我改用wait_until配合一个合理的截止时间,才解决。
看一个带超时处理的例子:
#include <iostream>
#include <future>
#include <chrono>
int main() {
auto future = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(5));
return 100;
});
// 最多等3秒
auto status = future.wait_for(std::chrono::seconds(3));
if (status == std::future_status::ready) {
std::cout << "结果: " << future.get() << std::endl;
} else if (status == std::future_status::timeout) {
std::cout << "超时了,任务还没完成" << std::endl;
} else {
std::cout << "任务被延迟执行了" << std::endl;
}
return 0;
}
Lambda与异步任务的完美结合
Lambda表达式在异步编程中简直是天作之合。你想想看,传统方式你得定义一个函数,或者写一个仿函数类,代码分散在各处。而Lambda可以就地定义,捕获上下文变量,干净利落。
我建议这样组织异步任务:
#include <iostream>
#include <future>
#include <vector>
#include <numeric>
int main() {
std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 1);
// 把大任务拆成两半,异步执行
auto mid = data.size() / 2;
auto future1 = std::async(std::launch::async, [&data, mid]() {
return std::accumulate(data.begin(), data.begin() + mid, 0LL);
});
auto future2 = std::async(std::launch::async, [&data, mid]() {
return std::accumulate(data.begin() + mid, data.end(), 0LL);
});
// 合并结果
long long sum = future1.get() + future2.get();
std::cout << "总和: " << sum << std::endl;
return 0;
}
这里要注意:Lambda捕获的是引用,所以data的生命周期必须长于异步任务。我曾经踩过这个坑——在函数返回后,异步任务还在访问局部变量,结果就是未定义行为。
std::make_shared创建共享数据,让Lambda捕获智能指针。
异常处理与异步任务
异步任务中抛出的异常,会在调用get()时重新抛出。这意味着你可以在主线程中统一处理异常,而不需要在子线程里处理。
#include <iostream>
#include <future>
#include <stdexcept>
int main() {
auto future = std::async(std::launch::async, []() {
throw std::runtime_error("任务执行出错");
return 0;
});
try {
int result = future.get();
std::cout << "结果: " << result << std::endl;
} catch (const std::exception& e) {
std::cout << "捕获到异常: " << e.what() << std::endl;
}
return 0;
}
嗯,这里要提醒一下:如果get()没有被调用,异常会被忽略。所以一定要确保每个future最终都被get()或wait()处理过。
std::shared_future:多线程共享结果
有时候,一个异步任务的结果需要被多个线程使用。这时候std::future就不够用了——它只能被get()一次。解决方案是std::shared_future。
#include <iostream>
#include <future>
#include <thread>
#include <vector>
int main() {
// 创建一个promise,手动设置值
std::promise<int> prom;
std::shared_future<int> shared_future = prom.get_future().share();
// 启动多个线程等待结果
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back([shared_future, i]() {
int value = shared_future.get();
std::cout << "线程 " << i << " 获取到: " << value << std::endl;
});
}
// 主线程设置结果
std::this_thread::sleep_for(std::chrono::seconds(1));
prom.set_value(42);
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
return 0;
}
这个模式在「发布-订阅」场景中特别有用。一个生产者产生数据,多个消费者同时处理。
知识体系总览
下面这张图总结了本章的核心概念和它们之间的关系:
最佳实践与避坑指南
最后,分享几个我在实际项目中积累的经验:
- 不要滥用异步:如果任务本身很轻量(比如几微秒就完成),创建线程的开销反而更大。我一般只对耗时超过1毫秒的任务使用
std::async。 - 注意资源限制:同时启动太多异步任务会导致线程爆炸。可以用线程池来限制并发数。
- 避免死锁:如果异步任务内部又调用了
get()等待另一个future,可能会形成死锁。我曾经在一个回调链里遇到过这个问题,排查了半天。 - 善用Lambda捕获:值捕获适合小数据,引用捕获适合大数据但要注意生命周期。移动捕获(C++14)适合转移所有权。
好了,这一章的内容就到这里。函数式并发其实没那么复杂,关键是理解「异步任务」这个抽象概念。你只要把任务扔出去,然后等结果回来就行——剩下的交给标准库去处理。
公众号:蓝海资料掘金营,微信deep3321