C++11:异步库 future


在多线程编程中,经常需要在多个线程间传递数据,这往往是一个比较棘手的任务。因为多线程间是异步执行的,你无法确定何时可以拿到数据,另外对于一个数据的读写,也要考虑线程安全问题。

C++11 引入了异步编程相关的一整套组件:std::asyncstd::futurestd::promisestd::packaged_taskstd::shared_future。它们提供了线程间的任务提交、结果获取、异常传递与结果共享等能力,需要头文件<future>


promise & future

std::promise<T> 允许生产者线程在未来某个时刻设置一个值/异常;消费者通过与之关联的 std::future<T> 获取结果。

promisefuture 分别被两个线程持有,持有 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::asyncstd::packaged_task不会创建一个新的线程,也不会立刻执行函数,只是单纯封装一个异步函数对象,自由度更高,使用起来也稍微复杂。

例如说,在一个线程池中,用户已经有线程了,只需要把任务传进去调用,此时就使用 packaged_task,因为它不会创建新的线程。


shared_future

std::futureget() 只能调用一次,也不支持拷贝。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::futurestd::promise等异步编程组件的基本用法,现在我们深入探讨一下它们的底层实现原理。

在编程实践中,我们经常需要使用异步调用。通过异步调用,我们可以将一些耗时、阻塞的任务交给其他线程来执行,从而保证当前线程的快速响应能力。同时,我们也可以通过将一个大任务分解成多个部分,然后交给多个线程来并发执行,从而提高应用性能。

异步调用的本质:当前线程将一个任务交给另外一个线程来执行,当前线程继续执行自己的任务,不需要等待异步任务的结果,直到在未来的某个时刻需要使用这个异步调用结果时

创建异步调用时会发生什么?其他线程执行异步调用并返回结果,异步调用创建方在需要时获取这个结果。

C++针对上述需求,提供了std::futurestd::promise两个核心对象。它们的关系如下:
在这里插入图片描述

如上图所示:

  • 异步调用创建方:持有std::future对象实例
  • 异步调用执行方:持有std::promise对象实例
  • 共享状态对象:在两者之间构建了一个信息同步的通道(channel)

说信道这个词也许有点抽象,可以理解为,这个共享状态对象本质就是在多个线程中可以同时看到的对象,并且有的线程可以往对象里面写数据,有的线程可以从对象中读取数据,从而实现线程间的通信,逻辑上可以说成信道。

异步调用执行方通过std::promise来兑现承诺(设置结果值),异步调用创建方通过std::future来获取这个未来的值

接下来针对futurepromise,深入源码看看它们是如何实现的。


promise

首先看promise的数据成员:

template<class _Rp>
class promise {
private:
    __assoc_state<_Rp>* __state_;  // 指向关联状态对象的指针
};

promise的内部结构非常简洁,只有一个核心成员__state_。这个成员变量是__assoc_state<_Rp>*类型的指针,指向一个关联状态对象。这个对象就是promisefuture之间的通信管道。其中_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最重要的方法,它的执行过程包括:

  1. 创建unique_ptr临时对象管理状态对象的生命周期(RAII机制)
  2. 保存当前状态对象指针到临时变量__s
  3. __state_置空,断开future与状态对象的连接
  4. __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 防止重复释放。


关联状态对象

关联状态对象是整个异步机制的核心,它采用三层继承结构,每层负责不同的职责:

在这里插入图片描述

  1. __shared_count:引用计数类,该类用来保存引用计数信息,通过该类内部的引用计数信息来实现自身对象生命周期的管理。用来跟踪链接到自身的promisefuture对象的数量,当没有任何对象链接自身的时候,进行自身资源的释放
  2. __assoc_sub_state:负责保存管理当前关联状态对象的状态,进行线程之间的同步
  3. __assoc_state:负责保存异步操作返回值,并且做最终的封装提供最终的接口给futurepromise来使用

__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进行完美转发,决定是移动还是拷贝。关键步骤:

  1. 加锁保护并发安全
  2. 检查是否已经设置过值,避免重复设置
  3. 使用placement new在预分配的__value_上构造结果对象
  4. 设置状态位为已构造和就绪
  5. 通知所有等待的线程

此处的 this->__mut_this->__cv_ 等是通过继承得到的锁和条件变量,来完成promisefuture之间的线程安全以及同步。

此外,_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(),就是这个函数。

  1. 加锁保护内部数据
  2. 调用__sub_wait()等待状态就绪(如果未就绪会阻塞)
  3. 检查是否有异常,有则重新抛出,实现跨线程异常传递
  4. 以移动语义返回结果

此处进入 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

为什么要区分__constructedready?有时候我们希望设置了结果后不立即变为就绪状态,比如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

  • promisefuture关联状态对象时,调用__add_shared()
  • 当它们断开连接时,调用__release_shared()
  • 当引用计数减到0时,自动销毁状态对象

这个很好理解,就不深入了。


总结

看完了promisefuture的源码,相信你对这个异步库的理解深入了不少。实际上就是让多个线程看到同一个关联对象,并在关联对象内部基于 mutexcondition_variable 实现线程的同步与互斥机制,并没有想象的那么复杂。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

盒马盒马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值