C++ 异步编程之future与promise、async、packaged_task

并发编程 Vs. 异步编程

特性并发编程异步编程
核心概念多线程、多进程非阻塞操作,常使用回调、Promises/Futures
主要应用场景提高CPU的利用效率,适用于计算密集型任务提高I/O效率,保持程序响应性,适用于I/O密集型任务
资源管理需要手动管理线程间的资源共享与数据一致性系统自动处理任务调度和资源分配,有效减少复杂性
主要工具std::threadstd::mutexstd::lockstd::condition_variablestd::futurestd::asyncstd::promise, 回调函数
设计和编写难度复杂,尤其在管理和调试多线程同步和资源共享时相对简单,通过非阻塞操作和事件驱动模型处理复杂任务

std::future 

概念定义

        std::thread 是C++11中提供异步创建多线程的工具,只能是异步运行任务,却无法获取任务执行的结果,一般都是依靠全局对象,全局对象在多线程下是及其不安全的,为此标准库提供了std::future类模板来关联线程运行的函数和函数的返回结果,这种获取结果的方式是异步的。

        std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

        一个有效的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,他们分别是:

  • std::async 函数
  • std::promise::get_future,get_future 为 promise 类的成员函数
  • std::packaged_task::get_future,此时 get_future为 packaged_task 的成员函数

        一个 std::future 对象只有在有效(valid)的情况下才有用(useful),由 std::future 默认构造函数创建的 future 对象不是有效的(除非当前非有效的 future 对象被 move 赋值另一个有效的 future 对象)。

std::future成员函数

template<typename ResultType>
class future
{
public:
  future() noexcept;
  future(future&&) noexcept;
  future& operator=(future&&) noexcept;
  ~future();
  
  future(future const&) = delete;
  future& operator=(future const&) = delete;


  bool valid() const noexcept;
  
  ResultType get();
  shared_future<ResultType> share();

  void wait();

  template<typename Rep,typename Period>
  future_status wait_for(
      std::chrono::duration<Rep,Period> const& relative_time);

  template<typename Clock,typename Duration>
  future_status wait_until(
      std::chrono::time_point<Clock,Duration> const& absolute_time);
};

构造函数

  • 不带参数的默认构造函数,此对象没有共享状态,因此它是无效的,但是可以通过移动赋值的方式将一个有效的future值赋值给它;
  • 禁用拷贝构造;
  • 支持移动构造。
  1.  std::future<int> fut;           // 默认构造函数
     fut = std::async(do_some_task);   // move-赋值操作。
    

析构函数

  • 销毁future对象,它是异常安全的。

get函数

  • 当共享状态就绪时,返回存储在共享状态中的值(或抛出异常)。
  • 如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
  • 当共享状态就绪后,则该函数将取消阻塞并返回(或抛出)释放其共享状态,这使得future对象不再有效,因此对于每一个future共享状态,该函数最多应被调用一次。
  • std::future<void>::get()(void特例化)不返回任何值,但仍等待共享状态就绪并释放它。
  • 共享状态是作为原子操作(atomic operation)被访问。

operator=

  • 禁用拷贝赋值。

  • 支持移动赋值:如果在调用之前,此对象是有效的(即它已经访问共享状态),则将其与先前已关联的共享状态解除关联。如果它是与先前共享状态关联的唯一对象,则先前的共享状态也会被销毁。

share函数

  • 获取共享的future,返回一个std::shared_future对象,该对象获取future对象的共享状态。调用该函数之后,该 std::future对象本身已经不和任何共享状态相关联,因此该std::future的状态不再是 valid 的了。

valid函数

  • 检查共享状态的有效性,返回当前的future对象是否与共享状态关联。一个有效的std::future对象只能通过 std::async()std::future::get_future 或者 std::packaged_task::get_future 来初始化。另外由 std::future 默认构造函数创建的 std::future 对象是无效(invalid)的,当然通过 std::future 的 move 赋值后该 std::future 对象也可以变为 valid。一旦调用了std::future::get()函数,再调用此函数将返回false。

wait函数

  • 等待共享状态就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪。
  • 当共享状态就绪后,则该函数将取消阻塞并返回。但是wait()并不读取共享状态的值或者异常。

wait_for函数

  • ready:共享状态已就绪;
  • timeout:在指定的时间内未就绪;
  • deferred:共享状态包含了一个延迟函数(deferred function)。
  • 等待共享状态在指定的时间内(time span)准备就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪或已达到设置的时间。
  • 此函数的返回值类型为枚举类future_status,此枚举类有三种label。

wait_until函数

  • 等待共享状态在指定的时间点(time point)准备就绪。如果共享状态尚未就绪(即提供者尚未设置其值或异常),则该函数将阻塞调用的线程直到就绪或已达到指定的时间点。
  • 此函数的返回值类型为枚举类future_status

代码示例 

#include <iostream>       // std::cout
#include <future>         // std::async, std::future
#include <chrono>         // std::chrono::milliseconds

// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
  for (int i=2; i<x; ++i) if (x%i==0) return false;
  return true;
}

int main ()
{
  // call function asynchronously:
  std::future<bool> fut = std::async (is_prime,444444443); 

  // do something while waiting for function to set future:
  std::cout << "checking, please wait";
  std::chrono::milliseconds span (100);
  while (fut.wait_for(span)==std::future_status::timeout)
    std::cout << '.' << std::flush;

  bool x = fut.get();     // retrieve return value

  std::cout << "\n444444443 " << (x?"is":"is not") << " prime.\n";

  return 0;
}

 

  • 使用 std::async 异步调用 is_prime 函数,参数为 444444443std::async 返回一个 std::future 对象 fut,可以用来查询异步任务的状态或获取其返回值。使用 std::async 异步执行任务,避免主线程阻塞。
  • 使用一个 std::chrono::milliseconds 对象 span 代表 100 毫秒的时间间隔。在一个循环中,每隔 100 毫秒检查一次异步任务的状态。如果任务还未完成(即返回 std::future_status::timeout),输出一个点符号,继续等待。
  • 当异步任务完成后,使用 get() 方法获取其返回值。

std::promise

概念

   std::promise是一个模板类: template<typename ResultType> class promise。其泛型参数ResultTypestd::promise对象保存的值的类型,例如std::promise<int>ResultType可以是void类型。std::promise类型模板提供设置异步结果的方法,这样其他线程就可以通过std::future实例来读取该结果。std::promisestd::future合作共同实现了多线程间通信。

        在构造std::promise对象时,该对象与新的共享状态(shared state)关联。通过调用std::promiseget_future函数,可以将该共享状态与std::future对象关联。调用get_future之后,两个对象共享相同的共享状态:std::promise对象是异步提供程序(asynchronous provider),应在某个时刻为共享状态设置一个值。std::future对象是个异步返回对象,可以检索共享状态的值,并在必要时等待其准备就绪。需要注意的是:set_value只能被调用一次,多次调用会抛出std::future_error异常。事实上std::promise::set_xxx函数会改变std::promise的状态为ready,再次调用时发现状态已是reday了,则抛出异常。

   std::promise实例是可以MoveConstructible(移动构造)和MoveAssignable(移动赋值),但是不能CopyConstructible(拷贝构造)和CopyAssignable(拷贝赋值)。

成员函数

template<typename ResultType>
class promise
{
public:
  promise();
  promise(promise&&) noexcept;
  ~promise();
  promise& operator=(promise&&) noexcept;

  template<typename Allocator>
  promise(std::allocator_arg_t, Allocator const&);

  promise(promise const&) = delete;
  promise& operator=(promise const&) = delete;

  void swap(promise& ) noexcept;
  
  std::future<ResultType> get_future();

  void set_value(see description);
  void set_exception(std::exception_ptr p);
};

构造函数

#include <iostream>       // std::cout
#include <functional>     // std::ref
#include <memory>         // std::allocator, std::allocator_arg
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

void print_int (std::future<int>& fut) {
  int x = fut.get();
  std::cout << "value: " << x << '\n';
}

int main ()
{
  std::promise<int> foo;
  std::promise<int> bar = std::promise<int>(std::allocator_arg,std::allocator<int>());

  std::future<int> fut = bar.get_future();

  std::thread th (print_int, std::ref(fut));

  bar.set_value (20);

  th.join();
  return 0;
}

 代码示例

#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>
 
void accumulate(std::vector<int>::iterator first,
                std::vector<int>::iterator last,
                std::promise<int> accumulate_promise)
{
    int sum = std::accumulate(first, last, 0);
    accumulate_promise.set_value(sum);  
}
 
void do_work(std::promise<void> barrier)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    barrier.set_value();
}
 
int main()
{
    // 用 promise<int> 在线程间传递结果
    std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
    std::promise<int> accumulate_promise;
    std::future<int> accumulate_future = accumulate_promise.get_future();
    std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
                            std::move(accumulate_promise));
 
    // future::get() 将等待直至该 future 拥有合法结果并取得它
    // 无需在 get() 前调用 wait()
    //accumulate_future.wait();  // 等待结果
    std::cout << "result=" << accumulate_future.get() << '\n';
    work_thread.join();  // wait for thread completion
 
    // 用 promise<void> 在线程间对状态发信号
    std::promise<void> barrier;
    std::future<void> barrier_future = barrier.get_future();
    std::thread new_work_thread(do_work, std::move(barrier));
    barrier_future.wait();
}

 

  •  accumulate 函数累加一个向量范围内的所有元素,并通过 std::promise<int> 将结果传递出去;
  • do_work 函数模拟一些工作,通过 std::promise<void> 向线程发送信号,指示工作已经完成;
  • 启动一个线程 work_thread,它执行 accumulate 函数,该函数将计算结果传递给 accumulate_promise;
  • 使用 accumulate_future.get() 获取结果并输出,这里不需要显式调用 wait(),因为 get() 会等待结果就绪。然后 join 等待线程完成;

std::packaged_task

概念定义

        std::packaged_task是一个模板类,它允许传入一个函数或其他可调用对象,并将函数计算的结果作为异步结果传递给std::future,包括函数运行时产生的异常。

代码示例

#include <iostream>
#include <cmath>
#include <thread>
#include <future>
#include <functional>
 
// unique function to avoid disambiguating the std::pow overload set
int f(int x, int y) { return std::pow(x,y); }
 
void task_lambda()
{
    std::packaged_task<int(int,int)> task([](int a, int b) {
        return std::pow(a, b); 
    });
    std::future<int> result = task.get_future();
 
    task(2, 9);
 
    std::cout << "task_lambda:\t" << result.get() << '\n';
}
 
void task_bind()
{
    std::packaged_task<int()> task(std::bind(f, 2, 11));
    std::future<int> result = task.get_future();
 
    task();
 
    std::cout << "task_bind:\t" << result.get() << '\n';
}
 
void task_thread()
{
    std::packaged_task<int(int,int)> task(f);
    std::future<int> result = task.get_future();
 
    std::thread task_td(std::move(task), 2, 10);
    task_td.join();
 
    std::cout << "task_thread:\t" << result.get() << '\n';
}
 
int main()
{
    task_lambda();
    task_bind();
    task_thread();
}

 

  • task_lambda 函数使用 lambda 表达式创建并调用一个 std::packaged_task
  • task_bind 函数使用 std::bind 创建并调用一个 std::packaged_task
  • task_thread 函数将 std::packaged_task 通过线程调用。

std::async

概念

        std::async是函数模板。std::thread使得我们可以非常方便的创建线程,执行异步任务,它的基本用法是这样的:

#include <iostream>

int add(int a, int b) { return a + b;}

int main() {
    std::thread t(add, 2, 3);
    t.join();
}

         这段代码新创建一个线程来计算两个数相加。但我们没办法获取线程的计算返回值。有时候使用thread有些不便,比如我希望获取线程函数的返回结果的时候,我就不能直接通过 thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后join,最后得到结果,这个过程是比较繁琐的。为此,c++11提供了异步接口std::async,通过这个异步接口可以很方便的获取线程函数的执行结果。std::async会自动创建一个线程去调用线程函数,它返回一个std::future,这个future中存储了线程函数返回的结果,当我们需要线程函数的结果时,直接从future中获取,非常方便。但是std::async提供的便利不仅仅是这一点,它首先解耦了线程的创建和执行,使得我们可以在需要的时候获取异步操作的结果;其次它还提供了线程的创建策略(比如可以通过延迟加载的方式去创建线程),使得我们可以以多种方式去创建线程。

        当调用std::async返回一个包含任务结果的std::future对象。根据策略,任务在其所在线程上是异步运行的,当有线程调用了这个future对象的wait()get()成员函数,则该任务会同步运行。有点类似封装了threadpackged_task的功能,使异步执行一个任务更为方便。

成员函数 

enum class launch
{
   async,  // 运行新线程来执行任务
    deferred   // 惰性求值,请求结果时才执行任务
};

template<typename Callable,typename ... Args>
future<result_of<Callable(Args...)>::type>
async(Callable&& func, Args&& ... args);

template<typename Callable,typename ... Args>
future<result_of<Callable(Args...)>::type>
async(launch policy, Callable&& func, Args&& ... args);

std::async是一个函数而非类模板,其函数执行完后的返回值绑定给std::futrue对象。func是要调用的可调用对象(function, member function, function object, lambda),Args是传递给Func的参数,std::launch policy是启动策略,它控制std::async的异步行为,我们可以用三种不同的启动策略来创建std::async

  • std::launch::async参数 保证异步行为,即传递函数将在单独的线程中执行;
  • std::launch::deferred参数 当其他线程调用get()/wait()来访问共享状态时,将调用非异步行为;
  • std::launch::async | std::launch::deferred参数 是默认行为(可省略)。有了这个启动策略,它可以异步运行或不运行,这取决于系统的负载。
#include <future>
#include <iostream>
#include <thread>
#include <chrono>

int entry() {
    std::cout <<"call entry" << std::endl;
    return 11;
}
int main() {
    std::future<int> the_answer=std::async(entry);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "The answer is " << the_answer.get()<<std::endl;
}

 

        与std::thread方式一样,std::async允许通过添加额外的调用参数,向函数传递额外的参数。第一个参数是指向成员函数的指针,第二个参数提供这个函数成员类的具体对象(是通过指针,也可以包装在std::ref中),剩余的参数可作为函数的参数传入。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。和std::thread一样,当参数为右值时,拷贝操作将使用移动的方式转移原始数据,就可以使用“只移动”类型作为函数对象和参数。

 

#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <future>

struct X
{
    void foo(int x, std::string const& str) {
        std::cout << "foo: " << x << " " << str <<std::endl;
    }
    std::string bar(std::string const& str) { 
        std::cout << "bar: " << str <<std::endl;
        return str;
    }
};
void test_1() {
    std::cout << "test_1" <<std::endl;
    X x;
    auto f1=std::async(&X::foo,&x, 42, "hello");  // 调用p->foo(42, "hello"),p是指向x的指针
    auto f2=std::async(&X::bar, x, "goodbye");  // 调用tmpx.bar("goodbye"), tmpx是x的拷贝副本
}

struct Y
{
  double operator()(double x) { return x; }
};
void test_2() {
    std::cout << "test_2" <<std::endl;
    Y y;
    auto f3=std::async(Y(), 3.141);  // 调用tmpy(3.141),tmpy通过Y的移动构造函数得到
    std::cout << "test_2 f3: " << f3.get() <<std::endl;
    auto f4=std::async(std::ref(y), 2.718);  // 调用y(2.718)
    std::cout << "test_2 f4: " << f4.get() <<std::endl;
}

X baz(X&) { return X(); }

void test_3() {
    std::cout << "test_3" <<std::endl;
    X x;
    auto f=std::async(baz,std::ref(x));  // 调用baz(x)
    f.get();
}

class move_only
{
public:
  move_only() {std::cout << "default\n";}
  move_only(move_only&&) {std::cout << "copy\n";}
  move_only(move_only const&) = delete;
  move_only& operator=(move_only&&) { std::cout << "operator=\n";return *this; }
  move_only& operator=(move_only const&) = delete;
  
  void operator()() {std::cout << "operator()\n";}
};

void test_4() {
    std::cout << "test_4" <<std::endl;
    // move_only()
    // 然后是std::move(move_only())构造得到
    // 最后是operator()
    auto f5=std::async(move_only());
}

int main() {
    test_1();
    test_2();
    test_3();
    test_4();
    std::cout << "finsh!" <<std::endl;
}

注意 

std::async 的返回值(std::future)在析构函数里会等待任务完成,如果不注意 std::async 的返回值很有可能就没有异步效果。

#include <iostream>
#include <chrono>
#include <future>

std::time_t now() 
{
    auto t0 = std::chrono::system_clock::now();
    std::time_t time_t_today = std::chrono::system_clock::to_time_t(t0);
    return time_t_today;  // seconds
}

void print() {
    std::cout << now() << " print start!" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << now() << " print end!" << std::endl;
}

int main() {
    {
        std::async(std::launch::async, print);
    }
    // wait the print finish here

    {
        auto f = std::async(std::launch::async, print);
    }
    // wait the print finish here

    std::cout << now() << " all finsh!" <<std::endl;
}

 

 二

使用默认策略却很方便,它不需要你显示指定的,或者你可以显示指定为std::launch::async | std::launch::deferred,c++标准中给出的说明是:进行异步执行还是惰性求值取决于实现:

auto future = std::async(func);        // 使用默认发射模式执行func

这种调度策略我们没有办法预知函数func是否会在哪个线程执行,甚至无法预知会不会被执行,因为func可能会被调度为推迟执行,即调用get或wait的时候执行,而get或wait是否会被执行或者在哪个线程执行都无法预知。

同时这种调度策略的灵活性还会混淆使用thread_local变量,这意味着如果func写或读这种线程本地存储(Thread Local Storage,TLS),预知取到哪个线程的本地变量是不可能的。

它也影响了基于wait循环中的超时情况,因为调度策略可能为deferred的,调用wait_for或者wait_until会返回值std::launch::deferred。

std::async意义

        在已经有了td::future、std::promise和std::packaged_task的情况下,实现异步或多线程间通信,可能觉得已经足够了,真的还要一个std::async来凑热闹吗?是std::async是为了让用户的少费点脑子的,它让这三个对象默契的工作。大概的工作过程是这样的:std::async先将异步操作用std::packaged_task包装起来,然后将异步操作的结果放到std::promise中,这个过程就是创造未来的过程。外面再通过future.get/wait来获取这个未来的结果。std::async真的是来帮忙的,你不用再想到底该怎么用std::future、std::promise和 std::packaged_task了,std::async已经帮你搞定一切了!这就是我们前面说的,std::async类似封装了threadpackged_task的功能。使得我们使用起来更加方便简单。

参考

C++ 多线程:std::async

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

**K

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

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

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

打赏作者

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

抵扣说明:

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

余额充值