C++11:异步库 future
在多线程编程中,经常需要在多个线程间传递数据,这往往是一个比较棘手的任务。因为多线程间是异步执行的,你无法确定何时可以拿到数据,另外对于一个数据的读写,也要考虑线程安全问题。
C++11 引入了异步编程相关的一整套组件:std::async
、std::future
、std::promise
、std::packaged_task
、std::shared_future
。它们提供了线程间的任务提交、结果获取、异常传递与结果共享等能力,需要头文件<future>
。
promise & future
std::promise<T>
允许生产者线程在未来某个时刻设置一个值/异常;消费者通过与之关联的 std::future<T>
获取结果。
promise
与 future
分别被两个线程持有,持有 promise
在计算出结果后,通过 promise.set_value()
设置结果。而持有 future
的线程通过 future.get()
拿到结果。
示例:
void set_value(std::promise<int> prom)
{
std::this_thread::sleep_for(std::chrono::seconds(5)); // 睡眠 5s
prom.set_value(10);
std::cout << "promise set value success" << std::endl;
}
void use_promise()
{
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(set_value, std::move(prom));
std::cout << "Waiting for the thread to set the value...\n";
std::cout << "Value set by the thread: " << fut.get() << "\n";
t.join();
}
以上代码首先创建了一个 std::promise<int>
对象 prom
,并通过 prom.get_future()
拿到它绑定的 future
对象。
随后创建一个线程跑 set_value
函数,把 prom
对象传进去。这个 promise
对象不能拷贝,只能移动。
在线程内部,先睡眠五秒模拟异步效果,随后把值设置为10
。在主线程中,会先执行 fut.get()
。如果此时 future
中的值还没有结果,就会一直阻塞到有结果为止,因此主线程也会阻塞五秒。
注意:
promise
只能与一个future
绑定一次;多次get_future()
会抛异常- 若
promise
先行销毁且未设置值,future.get()
会抛std::future_error
(broken promise)future.get()
只能调用一次
异常传递
promise
不仅可以传递一个数值,也可以传递异常。可以通过 promise.set_exception()
设置一个异常,对于接收端,依然使用future.get()
接受。
void risky_task(std::promise<int> prom)
{
try
{
throw std::invalid_argument("无效的参数值");
}
catch (...)
{
prom.set_exception(std::current_exception());
}
}
void test_exception()
{
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(risky_task, std::move(prom));
try
{
fut.get();
std::cout << "任务成功完成\n";
}
catch (const std::exception& e)
{
std::cerr << "捕获到异常: " << e.what() << '\n';
}
t.join();
}
在生产端通过 set_exception(std::current_exception()
设入异常,消费端在 future.get()
处重新抛出并被捕获,这样就完成了一个异常跨线程的传递。
promise.set_value()
与promise.set_exception()
只能择一调用一次
async
std::promise
是用户直接设置结果,而 std::async
可以直接启动一个线程,运行一个函数,并把函数的结果作为 std::future
返回。
std::async
函数声明如下:
template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
async (Fn&& fn, Args&&... args);
template <class Fn, class... Args>
future<typename result_of<Fn(Args...)>::type>
async (launch policy, Fn&& fn, Args&&... args);
看到第一个声明,它接收一个函数Fn
,以及参数包 Args
,把函数与参数包作为参数传给async
,此时async
会创建一个线程运行函数,并把args
作为参数传给函数。最后async
返回一个future
对象,它绑定到函数的返回值,此时外部就可以通过这个future
拿到函数的执行结果。
第二个声明中,多了一个参数 policy
,他表示函数的调用策略,分为三种:
std::launch::async
:新线程立即执行函数std::launch::deferred
:惰性执行,只有在调用future.get()
或future.wait()
时才在调用方线程中执行函数async | deferred
:编译期依据情况自动选择前两个方案之一
如果不指定,那么默认为 async | deferred
。
示例:
std::string getData(std::string str)
{
std::this_thread::sleep_for(std::chrono::seconds(5));
return str;
}
void use_async()
{
std::future<std::string> future_ret = std::async(std::launch::async, getData, "hello");
std::cout << "do something..." << std::endl;
std::string ret = future_ret.get();
std::cout << "data: " << ret << std::endl;
}
以上代码中,通过 std::async
创建一个异步任务执行 getData
函数,策略为 std::launch::async
表示立刻执行。此时会创建一个新线程运行该函数,并返回 future
对象,主线程依然通过 future.get()
获取到任务结果。
packaged_task
std::packaged_task
将任意可调用对象包装成新的可调用对象,其执行结果通过关联的 future
提取。
可以理解为,packaged_task
就是把一个函数对象包装成异步的形式,让这个函数返回一个future
。使用时,将要包装的可调用对象作为参数传入构造函数,将函数签名作为模板参数。
示例:
int calculate_sum(int a, int b)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
int result = a + b;
return result;
}
void test_package()
{
std::packaged_task<int(int,int)> task(calculate_sum);
std::future<int> result_future = task.get_future();
std::thread t(std::move(task), 10, 20);
std::cout << "do something..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "wait for result..." << std::endl;
int sum = result_future.get();
std::cout << "result: 10 + 20 = " << sum << std::endl;
t.join();
}
以上代码中,将函数 calculate_sum
封装为 task
,此时task
就是一个异步形式的函数对象了。使用get_future()
可以拿到它的future
对象。
随后将task
传入一个新的线程中执行,此处的packaged_task
也不允许拷贝,只能移动。
最后在主线程中通过future.get()
拿到函数的结果。
相比于std::async
,std::packaged_task
不会创建一个新的线程,也不会立刻执行函数,只是单纯封装一个异步函数对象,自由度更高,使用起来也稍微复杂。
例如说,在一个线程池中,用户已经有线程了,只需要把任务传进去调用,此时就使用 packaged_task
,因为它不会创建新的线程。
shared_future
std::future
的 get()
只能调用一次,也不支持拷贝。std::shared_future
则可以被复制到多个线程,并且每个线程都可以调用 get()
取相同的值。
int generate_result()
{
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
}
void print_result(std::shared_future<int> shared_fut, int thread_id)
{
std::cout << "线程 " << thread_id << " 等待结果...\n";
int result = shared_fut.get();
std::cout << "线程 " << thread_id << " 得到结果: " << result << "\n";
}
void test_shared()
{
std::shared_future<int> shared_fut = std::async(std::launch::async, generate_result);
std::thread t1(print_result, shared_fut, 1);
std::thread t2(print_result, shared_fut, 2);
std::thread t3(print_result, shared_fut, 3);
t1.join();
t2.join();
t3.join();
}
主线程首先创建一个 std::async
执行函数 generate_result
异步计算结果。此时返回的对象是一个std::future
,但是我们通过了一个std::shared_future
来接收,这是因为 shared_future
支持从一个 future
构造,并支持隐式转换:
shared_future(future<_Ty>&& _Other) noexcept
可以看到,shared_future
可以从一个 furure
移动而来,这也意味着原先的future
失效了。
随后创建了三个线程,每个线程都去读取shared_fut.get()
,这是合法的。
也可以先获得 std::future<int>
,再调用 fut.share()
转为 std::shared_future<int>
。
例如:
std::future<int> fut = std::async(std::launch::async, generate_result);
std::shared_future<int> shared_fut = fut.shared();
此外,shared_future
支持拷贝。
原理
在前面,我们学习了std::future
、std::promise
等异步编程组件的基本用法,现在我们深入探讨一下它们的底层实现原理。
在编程实践中,我们经常需要使用异步调用。通过异步调用,我们可以将一些耗时、阻塞的任务交给其他线程来执行,从而保证当前线程的快速响应能力。同时,我们也可以通过将一个大任务分解成多个部分,然后交给多个线程来并发执行,从而提高应用性能。
异步调用的本质:当前线程将一个任务交给另外一个线程来执行,当前线程继续执行自己的任务,不需要等待异步任务的结果,直到在未来的某个时刻需要使用这个异步调用结果时。
创建异步调用时会发生什么?其他线程执行异步调用并返回结果,异步调用创建方在需要时获取这个结果。
C++针对上述需求,提供了std::future
和std::promise
两个核心对象。它们的关系如下:
如上图所示:
- 异步调用创建方:持有
std::future
对象实例 - 异步调用执行方:持有
std::promise
对象实例 - 共享状态对象:在两者之间构建了一个信息同步的通道(channel)
说信道这个词也许有点抽象,可以理解为,这个共享状态对象本质就是在多个线程中可以同时看到的对象,并且有的线程可以往对象里面写数据,有的线程可以从对象中读取数据,从而实现线程间的通信,逻辑上可以说成信道。
异步调用执行方通过std::promise
来兑现承诺(设置结果值),异步调用创建方通过std::future
来获取这个未来的值。
接下来针对future
和promise
,深入源码看看它们是如何实现的。
promise
首先看promise
的数据成员:
template<class _Rp>
class promise {
private:
__assoc_state<_Rp>* __state_; // 指向关联状态对象的指针
};
promise
的内部结构非常简洁,只有一个核心成员__state_
。这个成员变量是__assoc_state<_Rp>*
类型的指针,指向一个关联状态对象。这个对象就是promise
和future
之间的通信管道。其中_Rp
是异步操作返回值的类型。
- 构造函数:
template<class _Rp>
promise<_Rp>::promise() : __state_(new __assoc_state<_Rp>) {}
构造函数很简单,通过new
操作符创建一个关联状态对象,用这个对象的地址初始化内部的__state_
指针,完成promise
与通道的连接。
- get_future():
template<class _Rp>
future<_Rp> promise<_Rp>::get_future() {
if (__state_ == nullptr)
__throw_future_error(future_errc::no_state);
return future<_Rp>(__state_);
}
该方法首先检查当前promise
是否关联了状态对象(判断__state_
是否为空),如果未关联则抛出异常。如果关联了状态对象,则用该对象的地址构造并返回一个future
对象。
- set_value():
template<class _Rp>
void promise<_Rp>::set_value(const _Rp& __r) {
if (__state_ == nullptr)
__throw_future_error(future_errc::no_state);
__state_->set_value(__r); // 委托给关联状态对象
}
template<class _Rp>
void promise<_Rp>::set_value(_Rp&& __r) {
if (__state_ == nullptr)
__throw_future_error(future_errc::no_state);
__state_->set_value(std::move(__r)); // 移动语义版本
}
这里有两个版本的set_value()
,分别对应左值和右值引用版本。方法先检查是否有关联状态对象,然后调用状态对象的set_value()
方法,将异步调用结果写入通道。
- 析构函数:
template<class _Rp>
promise<_Rp>::~promise() {
if (__state_) {
if (!__state_->__has_value() && __state_->use_count() > 1) {
// 设置 broken_promise 异常
__state_->set_exception(make_exception_ptr(
future_error(future_errc::broken_promise)));
}
__state_->__release_shared(); // 释放引用
}
}
析构函数会检查几个关键条件:
__state_->has_value()
:判断是否已设置结果或异常__state_->use_count() > 1
:判断是否还有其他对象(如future
)关联着这个状态
如果promise
析构时还没有设置值,且仍有future
等待结果,就会设置一个broken_promise
异常,表示承诺未兑现,后续其他future
读取这个结果,就会接收到这个异常。
future
future
的内部结构与promise
类似:
template<class _Rp>
class future {
private:
__assoc_state<_Rp>* __state_; // 指向关联状态对象的指针
};
两个类都通过内部的__state_
指针与通道关联,这个通道的实现就是__assoc_state
这个模板类。
- 构造函数:
template<class _Rp>
future<_Rp>::future(__assoc_state<_Rp>* __state) : __state_(__state) {
if (__state_)
__state_->__attach_future(); // 注册自己到状态对象
}
构造时不仅保存关联状态对象的地址,还调用__attach_future()
向状态对象注册自己,这样状态对象就知道有一个future
在等待结果。
- get():
template<class _Rp>
_Rp future<_Rp>::get() {
// 使用 RAII 管理状态对象的生命周期
unique_ptr<__shared_count, __release_shared_count> __(__state_);
__assoc_state<_Rp>* __s = __state_;
__state_ = nullptr; // 断开连接
return __s->move(); // 移动语义获取结果
}
这是future
最重要的方法,它的执行过程包括:
- 创建
unique_ptr
临时对象管理状态对象的生命周期(RAII机制) - 保存当前状态对象指针到临时变量
__s
- 将
__state_
置空,断开future
与状态对象的连接 __s->move()
调用状态对象的方法获取结果
__s->move()
会先判断是否有异步调用的值,如果异步调用没有完成,则会阻塞在move函数中,等待异步调用完成,如果完成了,那么就返回指定结果。
从这段代码可以看出,调用future.get()
的时候,__state_
会被置空,从而保证 get()
只能调用一次。
如果你智能指针学的不错,那么也很容易看出 unique_ptr
的使用也有蹊跷。__state_
的类型是 __assoc_state<_Rp>*
,为什么智能指针中第一个模板参数传入的却是 __shared_count
?
此外,对于以上的代码,unique_ptr
定义了一个删除器 __release_shared_count
,也就是说当生命周期结束,会调用这个函数对 __state_
做资源释放。
下面是用于释放引用计数的仿函数:
struct __release_shared_count {
void operator()(__shared_count* __p) {
__p->__release_shared(); // 减少引用计数
}
};
这里使用了多态,__assoc_state
是__shared_count
的派生类,因此 unique_ptr<__shared_count>
可以接收 __assoc_state
作为参数进行构造。
这个函数功能类似于shared_ptr
的引用计数,计算当前有多少个future
引用了这个__assoc_state
,如果没有future
了,就会释放掉 __assoc_state
内的结果,用于实现 shared_future
。
- 析构函数:
template<class _Rp>
future<_Rp>::~future() {
if (__state_)
__state_->__release_shared(); // 释放引用
}
析构时如果还有关联状态对象,就释放对它的引用。当引用计数减到0时,状态对象会自动销毁。
也就是说,对于一个future
,就算不调用get()
,也不用担心内存系列,当future
析构的时候,会自动释放这个 __state_
。如果调用了 get()
,那么也会把 __state_ = nullptr
防止重复释放。
关联状态对象
关联状态对象是整个异步机制的核心,它采用三层继承结构,每层负责不同的职责:
__shared_count
:引用计数类,该类用来保存引用计数信息,通过该类内部的引用计数信息来实现自身对象生命周期的管理。用来跟踪链接到自身的promise
和future
对象的数量,当没有任何对象链接自身的时候,进行自身资源的释放__assoc_sub_state
:负责保存管理当前关联状态对象的状态,进行线程之间的同步__assoc_state
:负责保存异步操作返回值,并且做最终的封装提供最终的接口给future
和promise
来使用
__assoc_state [数据存储]
这是最底层的派生类,负责存储异步操作的返回值:
template<class _Rp>
class __assoc_state : public __assoc_sub_state {
typedef __assoc_sub_state base;
typedef typename aligned_storage<sizeof(_Rp),
alignment_of<_Rp>::value>::type _Up;
protected:
_Up __value_; // 存储异步调用结果
};
它存储的 __value_
就是异步调用的结果,类型为 _Up
。
此处的 _Up
可以理解为是把实际存储的类型 _Rp
封装到了一个 aligned_storage
内部,可以利用内存对齐技术来优化访问效率。
- set_value():
template<class _Rp>
template<class _Arg>
void __assoc_state<_Rp>::set_value(_Arg&& __arg) {
unique_lock<mutex> __lk(this->__mut_); // 加锁保护
if (this->__has_value())
__throw_future_error(future_errc::promise_already_satisfied);
// 使用 placement new 在预分配内存上构造对象
::new(&__value_) _Rp(std::forward<_Arg>(__arg));
this->__state_ |= base::__constructed | base::ready; // 设置状态位
this->__cv_.notify_all(); // 唤醒等待的线程
}
这个方法是折叠引用版本,通过std::forward
进行完美转发,决定是移动还是拷贝。关键步骤:
- 加锁保护并发安全
- 检查是否已经设置过值,避免重复设置
- 使用placement new在预分配的
__value_
上构造结果对象 - 设置状态位为已构造和就绪
- 通知所有等待的线程
此处的 this->__mut_
、this->__cv_
等是通过继承得到的锁和条件变量,来完成promise
和future
之间的线程安全以及同步。
此外,_Up __value_
这个成员已经开辟好了内存,当往里面设置值,需要用到 定位 new
的语法,简而言之就是在指定的内存 new
一个对象,此处不深入讲解。
而 this->__state_
则是一个状态成员,也是继承得到的,它标识着当前的状态,比如是否设置好了值,稍后会深入讲解。
最后条件变量调用 notify_all()
唤醒所有等待的线程,比如曾经调用了future.get()
阻塞住的线程,收到信号后就知道__value_
已经设置好了,就会恢复活跃读取这个结果。
- move():
template<class _Rp>
_Rp __assoc_state<_Rp>::move() {
unique_lock<mutex> __lk(this->__mut_);
this->__sub_wait(__lk); // 等待就绪
if (this->__exception_)
rethrow_exception(this->__exception_); // 重新抛出异常
return std::move(*reinterpret_cast<_Rp*>(&__value_));
}
该方法用于获取存储的异步调用结果,你可以回去看future.get()
方法,最后调用的就是__s->move()
,就是这个函数。
- 加锁保护内部数据
- 调用
__sub_wait()
等待状态就绪(如果未就绪会阻塞) - 检查是否有异常,有则重新抛出,实现跨线程异常传递
- 以移动语义返回结果
此处进入 this->__sub_wait(__lk)
等待状态后,就要等到 promise
调用刚才的this->__cv_.notify_all()
才能从等待状态离开,拿到结果。如果你学习过C++的线程库,这一段应该很好理解。
- __on_zero_shared():
template<class _Rp>
void __assoc_state<_Rp>::__on_zero_shared() noexcept {
if (this->__state_ & base::__constructed)
reinterpret_cast<_Rp*>(&__value_)->~_Rp(); // 调用析构函数
delete this; // 释放自身内存
}
当引用计数减到0时调用此函数进行资源清理。注意这里只调用析构函数而不释放内存,因为__value_
是成员变量,会随着状态对象自身的销毁而释放。
__assoc_sub_state [状态管理]
__assoc_sub_state
负责状态管理和线程的互斥与同步:
class __assoc_sub_state : public __shared_count {
protected:
exception_ptr __exception_; // 保存异常信息
mutable mutex __mut_; // 互斥锁
mutable condition_variable __cv_; // 条件变量
unsigned __state_; // 状态位
};
各成员的作用:
__exception_
:保存异步调用抛出的异常,用于跨线程异常传递__mut_
:互斥锁,保证内部数据的线程安全__cv_
:条件变量,实现线程间的同步__state_
:状态位,记录当前对象的各种状态
状态位定义:
enum {
__constructed = 1, // bit0: 已构造结果值
__future_attached = 2, // bit1: 已附加 future
ready = 4, // bit2: 就绪状态
deferred = 8 // bit3: 延迟执行
};
每个状态位的含义:
__constructed
:表示内部已保存异步调用结果__future_attached
:表示有future
对象关联ready
:表示可以立即获取结果deferred
:表示延迟执行(用于std::launch::deferred
)
为什么要区分__constructed
和ready
?有时候我们希望设置了结果后不立即变为就绪状态,比如set_value_at_thread_exit()
函数:
template<class _Rp>
template<class _Arg>
void __assoc_state<_Rp>::set_value_at_thread_exit(_Arg&& __arg) {
unique_lock<mutex> __lk(this->__mut_);
if (this->__has_value())
__throw_future_error(future_errc::promise_already_satisfied);
::new(&__value_) _Rp(std::forward<_Arg>(__arg));
this->__state_ |= base::__constructed; // 只设置已构造,不设置就绪
__thread_local_data()->__make_ready_at_thread_exit(this); // 线程退出时才就绪
}
这个函数在线程退出时才将状态对象设为就绪,提供了另一种线程同步方法。
在 promise
中就继承这个方法:
void set_value_at_thread_exit (const T& val);
void set_value_at_thread_exit (T&& val);
可以代替set_value()
直接使用。
- __has_value():
bool __assoc_sub_state::__has_value() const {
return (__state_ & __constructed) || (__exception_ != nullptr);
}
判断是否有值:要么设置了结果值,要么设置了异常信息。
- __attach_future():
void __assoc_sub_state::__attach_future() {
lock_guard<mutex> __lk(__mut_);
if (__state_ & __future_attached)
__throw_future_error(future_errc::future_already_retrieved);
this->__add_shared(); // 增加引用计数
__state_ |= __future_attached; // 设置已附加标志
}
future
构造时调用此函数,确保一个状态对象只能被一个future
关联,并增加引用计数。
如果一个future
构造的时候,传入了一个已经绑定的关联状态对象,那么if (__state_ & __future_attached)
就会触发,导致抛出异常。
- __sub_wait():
还记得之前std::async
可以传入多种策略吗?可以立即执行,也可以等到获取结果时执行。
在__assoc_state
获取结果时,会调用move()
函数,而move()
内就会调用父类__assoc_sub_state
的__sub_wait
函数。
在 __sub_wait
函数内部,会处理 deferred
的惰性逻辑。
void __assoc_sub_state::__sub_wait(unique_lock<mutex>& __lk) {
if (!(__state_ & ready)) {
if (__state_ & deferred) {
__state_ &= ~deferred;
__lk.unlock();
__execute(); // 在当前线程执行延迟任务
} else {
while (!(__state_ & ready))
__cv_.wait(__lk); // 阻塞等待就绪通知
}
}
}
首先检测当前的关联状态是否处于就绪状态,随后判断是否被标记为了 deferred
。如果是,那么把deferred
这个状态清除掉,随后调用 __execute()
执行预设的函数。这样就可以实现第一次future.get()
的时候调用指定函数。
如果没有 deferred
这个状态,有可能是设置了立即执行,也有可能是之前已经有其他人第一次执行了get()
函数。那么此时__cv_.wait(__lk)
只要进入条件变量下等待唤醒即可。
__shared_count [引用计数层]
这一层维护引用计数,管理对象生命周期:
class __shared_count {
private:
long __shared_owners_; // 引用计数
public:
void __add_shared(); // 增加引用计数
void __release_shared(); // 减少引用计数
};
工作原理类似于shared_ptr
:
- 当
promise
和future
关联状态对象时,调用__add_shared()
- 当它们断开连接时,调用
__release_shared()
- 当引用计数减到0时,自动销毁状态对象
这个很好理解,就不深入了。
总结
看完了promise
和future
的源码,相信你对这个异步库的理解深入了不少。实际上就是让多个线程看到同一个关联对象,并在关联对象内部基于 mutex
和 condition_variable
实现线程的同步与互斥机制,并没有想象的那么复杂。