C++线程 future使用,std::async、std::packaged_task、std::promise

C++标准程序库使用future来模拟这类一次性事件:若线程需等待某个特定的一次性事件发生,则会以恰当的方式取得一个future,它代表目标事件。

C++标准程序库有两种future,分别由两个类模板实现,其声明位于标准库的头文件内:独占future(unique future,即std::future<>)和共享future(shared future,即std::shared_future<>)。

std::future

因为std::thread没有提供直接回传结果的方法,所以函数模板std::async()应运而生(其声明也位于头文件中)。

  • 使用std::async()按异步方式启动任务。我们从std::async()函数处获得std::future对象(而非std::thread对象),运行的函数一旦完成,其返回值就由该对象最后持有。

  • 若要用到这个值,只需在future对象上调用get(),当前线程就会阻塞,以便future准备妥当并返回该值。

  • 调用std::async()时,它可以接收附加参数,进而传递给任务函数作为其参数,此方式与std::thread的构造函数相同

    #include 
    #include 
    int find_the_answer_to_ltuae();
    void do_other_stuff();
    int main()
    {
        std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
        do_other_stuff();
        std::cout<<"The answer is "<<the_answer.get()<<std::endl;
    }
    

std::async()

  • std::async()的具体实现会自行决定等待future时,是启动新线程,还是同步执行任务

  • 可以设置根据std::launch类型的参数,其值可以是std::launch::deferred或std::launch::asyn同步还是异步。

  • std::launch::deferred指定在当前线程上延后调用任务函数,等到在future上调用了wait()或get(),任务函数才会执行

  • std::launch::asyn指定必须另外开启专属的线程,在其上运行任务函数。

  • std::launch::deferred | std::launch:: async,表示由std::async()的实现自行选择运行方式。最后这项是参数的默认值

    auto f1=std::async(std::launch::async,func());---  ①运行新线程
    
    auto f2=std::async(std::launch::deferred,func());---  ②在wait()get()内部运行任务函数
    
    auto f3=std::async(std::launch::deferred | std::launch::async,func());---  
    auto f4=std::async(func());---  ③交由实现自行选择运行方式
    
    f2.wait();---  ④前面②处的任务函数调用被延后,到这里才运行
    

凭借std::async(),即能简便地把算法拆分成多个子任务,且可并发运行。

使std::future和任务关联并非唯一的方法:运用类模板 std::packaged_task<> 的实例,我们也能将任务包装起来;又或者,利用 std::promise<> 类模板编写代码,显式地异步求值。

std::packaged_task<>

std::packaged_task<>对象在执行任务时,会调用关联的函数(或可调用对象),把返回值保存为future的内部数据,并令future准备就绪。

它可作为线程池的构件单元.若一项庞杂的操作能分解为多个子任务,则可把它们分别包装到多个std::packaged_task<>实例之中,再传递给任务调度器或线程池。

std::packaged_task<>是类模板,其模板参数是函数签名(function signature)。具有成员函数get_future(),它返回std::future<>实例,该future的特化类型取决于函数签名所指定的返回值。

  • 在线程间传递任务
    • 许多图形用户界面(Graphical User Interface,GUI)框架都设立了专门的线程,作为更新界面的实际执行者。若别的线程需要更新界面,就必须向它发送消息,由它执行操作。该模式可以运用std::packaged_task实现,如下代码所示。

    • #include 
      #include 
      #include 
      #include 
      #include 
      
      std::mutex m;
      std::deque<std::packaged_task<void()>> tasks;
      bool gui_shutdown_message_received();
      void get_and_process_gui_message();
      void gui_thread()---{
          while(!gui_shutdown_message_received())---{
              get_and_process_gui_message();---  ③
              std::packaged_task<void()> task;
              {
                  std::lock_guard<std::mutex> lk(m);
                  if(tasks.empty())---continue;
                  task=std::move(tasks.front());---  ⑤
                  tasks.pop_front();
              }
              task();---}
      }
      std::thread gui_bg_thread(gui_thread);
      template<typename Func>
      std::future<void> post_task_for_gui_thread(Func f)
      {
          std::packaged_task<void()> task(f);---  ⑦
          std::future<void> res=task.get_future();---  ⑧
          std::lock_guard<std::mutex> lk(m);
          tasks.push_back(std::move(task));---return res;---}
      
    • 在GUI线程上①,轮询任务队列和待处理的界面消息(如用户的单击)③;若有消息指示界面关闭,则循环终止②。假如任务队列一无所有,则循环继续④;否则,我们就从中取出任务⑤,释放任务队列上的锁,随即运行该任务⑥。在任务完成时,与它关联的future会进入就绪状态。

    • 向任务队列布置任务也很简单。我们依据给定的函数创建新任务,将任务包装在内⑦,并随即通过调用成员函数get_future(),取得与该任务关联的future⑧,然后将任务放入任务队列⑨,接着向post_task_for_gui_thread()的调用者返回future⑩。接下来,有关代码向GUI线程投递消息,假如这些代码需判断任务是否完成,以获取结果进而采取后续操作,那么只要等待future就绪即可;否则,任务的结果不会派上用场,关联的future可被丢弃。

    • std::async用于创建异步任务,std::packaged_task则将一个可调用对象(包括函数、函数对象、lambda表达式、std::bind表达式、std::function对象)进行包装,以便该任务能被异步调用(即在其他线程中调用)。
      二者均可通过std::future对象返回执行结果。二者使用的一个主要差别是:std::packaged_task需要等待执行结果返回,而std::async不必。

有些任务无法以简单的函数调用表达出来,还有一些任务的执行结果可能来自多个部分的代码。需运用第三种方法创建future:借助std::promise显式地异步求值。

std::promise

std::promise给出了一种异步求值的方法(类型为T),某个std::future对象与结果关联,能延后读出需要求取的值。

工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。

std::promise具有成员函数get_future() ,它返回std::future实例。。promise的值通过成员函数 set_value() 设置,只要设置好,future即准备就绪,凭借它就能获取该值。

如果std::promise在被销毁时仍未曾设置值,保存的数据则由异常代替。

//利用多个promise在单个线程中处理多个连接
#include 
void process_connections(connection_set& connections)
{
    while(!done(connections))---  ① 函数process_connections()反复循环,若done()返回true则停止
    {
        for(connection_iterator    ⇽---  ② 每轮循环中,程序依次检查各个连接
                connection=connections.begin(),end=connections.end();
            connection!=end;
            ++connection)
        {
            if(connection->has_incoming_data())---  ③ 若有数据传入,则接收
            {
                data_packet data=connection->incoming();
                std::promise<payload_type>& p=
                    connection->get_promise(data.id);---  ④ 假定传入的数据包本身已含有ID和荷载数据。令每个ID与各std::promise对象一一对应。将其相关值设置为数据包的有效荷载
                p.set_value(data.payload);
            }
            if(connection->has_outgoing_data())---  ⑤ 或者,若发送队列中有数据,则向外发送
            {
                outgoing_packet data=
                    connection->top_of_outgoing_queue();
                connection->send(data.payload);
                data.promise.set_value(true);---  ⑥ 向外发送的数据包取自发送队列,并通过连接发出。只要发送完成,与向外发送数据关联的promise就会被设置为true,示意数据发送成功
            }
        }
    }
}

将异常保存到future

  • 若经由std::async()调用的函数抛出异常,则会被保存到future中,代替本该设定的值,future随之进入就绪状态,等到其成员函数get()被调用,存储在内的异常即被重新抛出。
  • 把任务函数包装在std::packaged_task对象内,也依然如是。若包装的任务函数在执行时抛出异常,则会代替本应求得的结果,被保存到future内并使其准备就绪。只要调用get(),该异常就会被再次抛出。
  • std::promise也具有同样的功能,它通过成员函数的显式调用实现。假如我们不想保存值,而想保存异常,就不应调用set_value(),而应调用成员函数set_exception()。

std::future自身存在限制,它只容许一个线程等待结果。若我们要让多个线程等待同一个目标事件,则需改用std::shared_future。

你可能感兴趣的:(C++基础,c++11,多线程,c++)