告别回调地狱:Folly异步编程模型轻松掌握
你是否还在为复杂的异步回调逻辑焦头烂额?是否在寻找一种简单高效的方式来处理并发任务?Facebook开源的C++库Folly(Folly)提供了强大的异步编程框架,让你轻松驾驭并发编程,告别回调地狱。本文将带你深入了解Folly的Futures/Promises模式与执行器框架,读完你将能够:
- 理解Futures和Promises的核心概念及工作原理
- 掌握Folly异步编程的基本使用方法
- 学会使用执行器框架管理线程和任务调度
- 解决实际开发中常见的异步编程问题
什么是Futures/Promises模式?
Futures/Promises模式是一种用于处理异步操作的编程范式,它将异步操作的结果与获取结果的过程分离,使得代码更加清晰和可维护。
在Folly中,Promise(承诺)是异步操作的生产者,它负责生成结果;而Future(未来)是异步操作的消费者,它负责获取结果。这种分离使得我们可以在不阻塞当前线程的情况下执行耗时操作,并在操作完成后得到通知。
Futures与Promises的关系
Promise和Future是一一对应的关系:一个Promise对应一个Future。当Promise设置了结果后,对应的Future就会得到通知,然后可以继续执行后续的回调函数。
Folly的Futures/Promises实现主要定义在以下头文件中:
- folly/futures/Future.h:定义了
Future和SemiFuture类 - folly/futures/Promise.h:定义了
Promise类
快速上手:Folly异步编程基础
让我们通过一个简单的例子来了解Folly异步编程的基本用法。
创建Promise和Future
首先,我们需要创建一个Promise和对应的Future:
#include <folly/futures/Promise.h>
#include <folly/futures/Future.h>
// 创建一个Promise和对应的SemiFuture
auto [promise, future] = folly::makePromiseContract<int>();
这里使用了folly::makePromiseContract函数来创建一个Promise<int>和一个SemiFuture<int>。SemiFuture是一种不包含执行器的轻量级Future,我们将在后面介绍如何将其转换为可以执行回调的Future。
设置Promise的结果
接下来,我们可以在另一个线程中设置Promise的结果:
#include <thread>
// 在新线程中执行耗时操作
std::thread([p = std::move(promise)]() mutable {
// 模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(1));
// 设置成功结果
p.setValue(42);
// 如果发生错误,可以设置异常
// p.setException(std::make_exception_ptr(std::runtime_error("操作失败")));
}).detach();
获取Future的结果
现在,我们可以通过Future来获取异步操作的结果。有两种方式可以获取结果:阻塞等待或者设置回调函数。
阻塞等待结果
try {
// 阻塞等待结果,会抛出异常如果Promise设置了异常
int result = std::move(future).via(folly::getGlobalCPUExecutor()).get();
std::cout << "异步操作结果: " << result << std::endl;
} catch (const std::exception& e) {
std::cout << "异步操作失败: " << e.what() << std::endl;
}
这里使用via(folly::getGlobalCPUExecutor())将SemiFuture转换为Future,并指定了执行回调的 executor。
设置回调函数
更常见的做法是设置回调函数,在结果可用时自动执行:
// 设置回调函数,当结果可用时执行
std::move(future)
.via(folly::getGlobalCPUExecutor())
.thenValue([](int result) {
std::cout << "异步操作成功,结果: " << result << std::endl;
})
.thenError([](const std::exception& e) {
std::cout << "异步操作失败: " << e.what() << std::endl;
});
thenValue方法用于处理成功的结果,thenError方法用于处理异常。这种链式调用的方式使得代码非常清晰。
SemiFuture与Future的区别
在Folly中,有两种类型的Future:SemiFuture和Future。
SemiFuture
SemiFuture是一种轻量级的Future,它不包含执行器(Executor),因此不能直接执行回调函数。它主要用于在不同的执行器之间传递异步操作的结果。
SemiFuture的主要特点:
- 不包含执行器,因此更轻量
- 可以转换为
Future - 主要用于表示异步操作的结果,而不关心如何执行回调
Future
Future是在SemiFuture的基础上增加了执行器的概念,它可以在指定的执行器上执行回调函数。
要将SemiFuture转换为Future,可以使用via方法:
SemiFuture<int> semiFuture = ...;
// 将SemiFuture转换为Future,使用全局CPU执行器
Future<int> future = std::move(semiFuture).via(folly::getGlobalCPUExecutor());
Folly提供了多种执行器,定义在folly/executors/目录下,包括:
CPUThreadPoolExecutor:用于CPU密集型任务IOThreadPoolExecutor:用于IO密集型任务GlobalExecutor:全局执行器,提供了默认的线程池
高级用法:链式操作与组合
Folly的Futures提供了丰富的操作符,可以轻松地组合多个异步操作,构建复杂的异步工作流。
链式操作
使用thenValue可以将多个异步操作串联起来:
// 链式操作示例
fetchData() // 返回Future<Data>
.thenValue([](Data data) {
// 处理数据,返回新的Future
return processData(data); // 返回Future<ProcessedData>
})
.thenValue([](ProcessedData result) {
// 保存结果,返回Future<Unit>表示完成
return saveResult(result); // 返回Future<Unit>
})
.thenValue([]() {
std::cout << "所有操作完成!" << std::endl;
})
.thenError([](const std::exception& e) {
std::cerr << "操作失败: " << e.what() << std::endl;
});
并行操作
使用collect可以等待多个Future完成:
// 并行操作示例
std::vector<Future<int>> futures;
for (int i = 0; i < 5; ++i) {
futures.push_back(fetchSomeData(i));
}
// 等待所有Future完成
collectAll(futures)
.thenValue([](std::vector<int> results) {
int sum = 0;
for (int result : results) {
sum += result;
}
std::cout << "所有结果之和: " << sum << std::endl;
});
除了collectAll,Folly还提供了其他组合多个Future的函数:
whenAll:等待所有Future完成,不关心结果firstCompletedOf:等待第一个完成的FutureanyOf:等待任意一个Future完成allOf:等待所有Future完成,返回一个表示成功或失败的Future
异常处理
在异步编程中,异常处理尤为重要。Folly的Futures提供了多种处理异常的方式。
使用thenError捕获异常
thenError方法可以捕获前一个Future抛出的异常:
fetchData()
.thenValue([](Data data) {
// 处理数据
})
.thenError(folly::tag_t<std::runtime_error>(), [](const std::runtime_error& e) {
// 处理运行时错误
std::cerr << "运行时错误: " << e.what() << std::endl;
return defaultData(); // 返回默认数据
})
.thenError(folly::tag_t<std::logic_error>(), [](const std::logic_error& e) {
// 处理逻辑错误
std::cerr << "逻辑错误: " << e.what() << std::endl;
throw; // 重新抛出异常
});
使用thenTry处理结果和异常
thenTry方法可以同时处理正常结果和异常:
fetchData()
.thenTry([](folly::Try<Data> tryData) {
if (tryData.hasValue()) {
// 处理正常结果
return processData(tryData.value());
} else {
// 处理异常
std::cerr << "获取数据失败: " << tryData.exception().what() << std::endl;
return defaultData();
}
});
folly::Try<T>是一个可以保存值或异常的类,定义在folly/Try.h中。
执行器框架
Folly的执行器框架负责管理线程和任务调度,它是Futures能够异步执行回调的基础。
执行器的类型
Folly提供了多种执行器,以满足不同的需求:
| 执行器类型 | 用途 | 头文件 |
|---|---|---|
CPUThreadPoolExecutor | CPU密集型任务 | folly/executors/CPUThreadPoolExecutor.h |
IOThreadPoolExecutor | IO密集型任务 | folly/executors/IOThreadPoolExecutor.h |
GlobalExecutor | 全局默认执行器 | folly/executors/GlobalExecutor.h |
InlineExecutor | 内联执行任务(同步执行) | folly/executors/InlineExecutor.h |
使用全局执行器
Folly提供了全局执行器,可以直接使用:
// 获取全局CPU执行器
auto executor = folly::getGlobalCPUExecutor();
// 将SemiFuture转换为Future,使用全局执行器
Future<int> future = semiFuture.via(executor);
自定义执行器
我们也可以创建自定义的执行器:
// 创建一个CPU线程池执行器,包含4个线程
auto executor = std::make_shared<folly::CPUThreadPoolExecutor>(4);
// 使用自定义执行器
future.via(executor->keepAlive())
.thenValue([](int result) {
// 在自定义执行器的线程中执行
});
实际应用:异步日志系统
让我们通过一个实际的例子来展示Folly异步编程的强大之处。我们将实现一个简单的异步日志系统。
异步日志系统设计
异步日志系统的核心思想是将日志写入操作与业务逻辑分离,避免日志写入阻塞业务线程。
#include <folly/futures/Promise.h>
#include <folly/futures/Future.h>
#include <folly/executors/CPUThreadPoolExecutor.h>
#include <queue>
#include <mutex>
#include <fstream>
#include <string>
class AsyncLogger {
public:
AsyncLogger(const std::string& filename)
: executor_(std::make_shared<folly::CPUThreadPoolExecutor>(1)) {
// 启动日志写入线程
startWriterThread(filename);
}
// 异步写入日志
folly::Future<folly::Unit> log(const std::string& message) {
auto [promise, future] = folly::makePromiseContract<folly::Unit>();
std::lock_guard<std::mutex> lock(queueMutex_);
logQueue_.emplace(std::move(promise), message);
// 通知写入线程有新日志
cv_.notify_one();
return future.via(executor_);
}
private:
struct LogItem {
folly::Promise<folly::Unit> promise;
std::string message;
};
void startWriterThread(const std::string& filename) {
// 在单独的线程中写入日志
writerThread_ = std::thread([this, filename]() {
std::ofstream logFile(filename, std::ios::app);
if (!logFile.is_open()) {
std::cerr << "无法打开日志文件: " << filename << std::endl;
return;
}
std::queue<LogItem> localQueue;
while (running_) {
{
std::unique_lock<std::mutex> lock(queueMutex_);
cv_.wait(lock, [this]() {
return !logQueue_.empty() || !running_;
});
if (!running_) break;
// 将日志项移动到本地队列,减少锁持有时间
logQueue_.swap(localQueue);
}
// 写入所有日志
while (!localQueue.empty()) {
auto item = std::move(localQueue.front());
localQueue.pop();
// 写入日志
logFile << "[" << getCurrentTime() << "] " << item.message << std::endl;
// 通知日志写入完成
item.promise.setValue();
}
// 刷新文件
logFile.flush();
}
});
}
// 获取当前时间字符串
std::string getCurrentTime() {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
return std::ctime(&time);
}
std::shared_ptr<folly::CPUThreadPoolExecutor> executor_;
std::thread writerThread_;
std::queue<LogItem> logQueue_;
std::mutex queueMutex_;
std::condition_variable cv_;
bool running_ = true;
};
使用异步日志系统
// 创建异步日志器
AsyncLogger logger("app.log");
// 异步写入日志
logger.log("应用启动")
.thenValue([]() {
std::cout << "日志写入成功" << std::endl;
})
.thenError([](const std::exception& e) {
std::cerr << "日志写入失败: " << e.what() << std::endl;
});
// 业务逻辑...
总结与展望
Folly的Futures/Promises模式为C++异步编程提供了强大而灵活的工具。通过将异步操作的结果与获取结果的过程分离,我们可以编写出更加清晰、可维护的并发代码。
本文介绍了Folly异步编程的基础知识,包括Futures/Promises模式、执行器框架、异常处理等内容,并通过实际例子展示了如何应用这些知识解决实际问题。
未来,随着C++标准的不断发展,Folly的异步编程模型也将不断演进。我们可以期待更多新特性的加入,如对C++20协程的更好支持,以及更高效的任务调度算法。
参考资料
希望本文能够帮助你更好地理解和应用Folly的异步编程模型。如果你有任何问题或建议,欢迎在评论区留言讨论!
点赞、收藏、关注三连,获取更多C++异步编程技巧!下期预告:《Folly并发容器实战指南》
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



