pthread线程传递数据回主线程_你好,C++(82)12.3.2 利用future和promise简化线程的返回值操作...

13991eecd0cca839d36d1d068a676b92.png
想要抢先看后面的章节?打赏本文10元,即可获得带插图全本下载地址!打赏完成记得私信我哦 :p

12.3.2 利用future和promise简化线程的返回值操作

在上面的小节中,我们介绍了如何利用thread创建线程并执行一个线程函数,也介绍了如何利用参数向线程函数内传入或是从线程函数内传出数据,这一切看起来都简单得很。然而,现实当中的多线程应用场景是非常复杂的,上面小节中介绍的方法,特别是利用传指针或传引用的参数形式向线程函数外传出数据的方法,只适用于哪些规规矩矩的只会在分支线程结束后才去获取结果数据的应用场景。更多时候,为了及时地得到结果数据,往往是还未等分支线程执行完毕,主线程就会试图访问用于保存结果数据的两个线程之间的共享变量。这时有可能发生的状况是,分支线程正在写入结果数据到共享变量,而主线程却也正在从共享变量读取结果数据,其结果往往是主线程读取到错误的结果数据。这就像在餐馆点菜,为了早点吃到菜,顾客(主线程)总是不断地催问厨子(分支线程)菜好了没菜好了没。最后的结果是顾客并没有早点吃到菜,反倒是浪费了自己的口水,或者是吃到了半生不熟的菜。这样的结果,谁都不愿意看到。为了更好地从线程函数及时返回结果数据,C++11提出了future和promise机制。

future和promise是一种可以在线程之间传递数据的较高层次的编程机制,它可以在线程之间传递数值数据,也可以传递异常,更方便的是,在分支线程的结果数据尚未准备完成的情况下,它还可以让主线程一直等待而不用去不断地查询结果数据是否已经准备完成。而一旦分支线程的结果数据准备完成,它又会及时地通知主线程来读取结果数据。有了这套机制,顾客将再也不用总是去催问厨子菜好了没,他只要下了单(promise),大可以在桌子边静静等着,而一旦厨子做好了菜,服务员(future)就会及时地把菜端到他的面前。整个过程没有任何延迟,也没有资源的竞争,从而很好地达到了从线程函数及时返回结果数据的目的。

在对线程函数结果数据的操作上,future提供的是get操作(通过get()函数实现),与之相对应的,promise提供的是set操作(通过set_value()函数或set_exception()函数实现)。这样有了明确的分工,future和promise相互协作共同从线程函数传出结果数据的基本思路就非常简单了:首先将future对象和promise对象配对,然后将promise对象传递给分支线程。当一个分支线程完成结果数据后,它就把这个数据通过set_value()函数放到promise对象中;之后,这个结果数据就会出现在和此promise对象关联的future对象中。最终通过这个future对象的get()成员函数,我们就可以读取到从线程函数中传递出来的结果数据。例如,我们可以用future和promise来解决上面那个餐馆点菜的问题:

#include <thread>
#include <future> // 引入future所在的头文件
#include <string>
#include <iostream>
 
using namespace std;
 
// 需要从线程函数传递出来的数据
class Food
{
public:
  Food(){} // 默认构造函数
  // 通过菜名构建Food对象
Food(string strName) : m_strName(strName){}
  // 获取菜名
string GetName() const
  {
 return m_strName;
}
private:
string m_strName; // 菜名
};
 
// 线程函数
// 根据菜名创建Food对象,并通过promise对象返回结果数据
void Cook(const string strName,promise<Food>& prom)
{
// 做菜…
Food food(strName);
// 将创建完成的food对象放到promise传递出去
prom.set_value(food);
}
 
int main()
{ 
// 用于存放结果数据的promise对象
promise<Food> prom;
// 获得promise所关联的future对象
future<Food> fu = prom.get_future();
 
// 创建分支线程执行Cook()函数
// 同时将菜名和用于存放结果数据的promise对象传递给Cook()函数
// ref()函数用于获取promise对象的引用
thread t(Cook,"回锅肉",ref(prom));
 
// 等待分支线程完成Food对象的创建,一旦完成,立即获取完成的Food对象
Food food = fu.get();
// 上菜
cout<<"客官,你点的"<<food.GetName()<<"来了,请慢用!"<<endl;
t.join(); // 等待分支线程最终完成
 
return 0;
}

future和promise实际上是两个类模板,在这里,我们首先根据它们所处理的结果数据,使用了相应的数据类型Food对其进行了特化而得到模板类future<Food>和promise<Food>,之后就是创建用于存放结果数据的promise对象prom,并通过get_future()函数从prom对象获得与之关联的future对象fu。这样,prom对象和fu对象就配对成功,也就意味着它们之间达成了某种约定(promise):在分支线程中,我们可以将结果数据set_value()到prom对象中,而在主线程中,我们可以从fu对象中get()到结果数据。接下来的工作,就是在创建线程执行线程函数的同时,将用于存放结果数据的prom对象传递给线程函数。而在线程函数的执行中,如果结果数据已经准备完成,它就会通过set_value()函数将结果数据存放到prom对象中。于此同时,主线程正在通过fu对象的get()函数等待分支线程的结果数据准备完成,一旦分支线程将准备完成的结果数据set_value()到prom对象,get()函数就会获取到分支线程存放在prom中的结果数据并返回。整个过程不需要主线程去不断查询,而分支线程一旦完成结果数据主线程就会立刻得到,从而很好地做到了结果数据的及时返回。整个过程如下图12-5所示:

f94831ae5b8ed687384449103be7fafb.png

图12-5 future和promise传递数据的过程

这里需要注意的是,一旦主线程开始调用future对象的get()函数等待分支线程返回结果数据,主线程的执行就会暂停下来,直到分支线程的结果数据准备完成并被set_value()到promise中,get()函数获得结果数据返回而主线程才会继续往下执行。可是有时候,分支线程的执行有可能会出现问题而导致结果数据迟迟没有准备完成。就像餐馆的生意实在太好厨子忙不过来,这时顾客的选择大多是等十分钟,如果十分钟后菜还没有好就直接走人换一家餐馆。这种情况表现在程序中就是,我们可以使用future对象的wait_for()成员函数让主线程等待一段时间,如果在这个时间内分支线程的结果数据没有准备好,它就会结束等待继续执行后面的代码。这样,如果分支线程发生错误而导致结果数据一直没有到达,wait_for()函数可以使得主线程不至于一直处于阻塞状态,从而有机会采取其他措施来解决问题。就像这里的顾客,在菜迟迟没有做好的情况下立刻换了一家餐馆,要不然会被活活饿死。例如:

#include <chrono> // 为了使用minutes类
// …
using namespace std::chrono; // 使用minutes所在的名字空间
// …
// 等待十分钟
if (fu.wait_for(minutes(10))) 
{ 
// 如果十分钟内结果数据准备完成,则从future对象中获取结果数据
Food food = fu.get();
cout<<"客官,你点的"<<food.GetName().c_str()<<"来了,请慢用!"<<endl;
} 
else // 如果十分钟内结果数据尚未到达
{
cout<<"等不下去了,换个餐馆"<<endl; 
}

future和promise的相互配合,是可以从线程函数及时返回结果数据,但是这个过程中我们既要完成它们之间的配对,又要传递promise对象给线程函数并自己动手完成结果数据的存放,整个过程稍显繁琐。为了简化这一过程,C++11特别地提供了packaged_task类模板。在大多数情况下,我们需要获得的线程函数的结果数据其实是它的返回值,这时,我们可以首先用这个线程函数的返回值和参数类型对packaged_task类模板进行特化以形成特定类型的packaged_task模板类,然后用这个线程函数作为其构造函数参数就可以创建得到一个packaged_task对象。而在这个对象中,就已经完成了future和promise的配对工作,我们只需要通过它的get_future()成员函数就可以得到配对完成的future对象,进而可以用它来获得线程函数的返回值。例如,我们可以用packaged_task将上面的例子简化为:

// … 
 
// 线程函数的返回值成了我们所需要的结果数据
// 不再需要向线程函数传递promise对象
Food Cook(const string strName)
{
  // 做菜…
Food food(strName);
  // 直接将结果数据通过返回值返回
return food;
}
 
int main()
{ 
// 使用线程函数的返回值和参数类型特化packaged_task类模板
// 利用其构造函数,将线程函数打包成一个packaged_task对象
packaged_task<Food(string)> cooker(Cook);
 
// 从packaged_task对象获得与之关联的future对象
future<Food> fu = cooker.get_future();
   // 创建线程执行packaged_task对象,实际上执行的是Cook()函数
// 这里也不再需要传递promise对象
 thread t(move(cooker),"回锅肉");
 
  // 同样地获得结果数据
Food food = fu.get();
 cout<<"客官,你点的"<<food.GetName()<<"来了,请慢用!"<<endl;
  t.join();
  
return 0;
} 

在这里,首先是线程函数发生了变化,我们通过线程函数Cook()的返回值来返回其结果数据,其返回值类型变成了Food类型,同时因为不需要传递用于存放结果数据的promise对象,其参数也少了promise类型。在主函数中,我们首先用线程函数Cook()的返回值和参数类型特化packaged_task类模板而得到一个模板类packaged_task<Food(string)>,然后用线程函数Cook()作为其构造函数参数而创建得到一个packaged_task对象,进而通过它的get_future()成员函数得到已经配对完成的future对象。随后,我们创建了新的线程来执行这个packaged_task,也就是执行线程函数Cook()开始准备结果数据。与此同时,我们使用future对象的get()函数来等待分支线程执行完毕。一旦分支线程的线程函数执行完毕返回结果数据Food对象,这个Food对象就会被主线程中的get()函数获得而返回。通过这样一个简单的过程,我们同样可以及时地获得线程函数Cook()所返回的Food对象。使用packaged_task类模板,确实起到了简化future和promise配对工作的目的,可是在这个过程中,我们仍然需要创建packaged_task对象,仍然需要从packaged_task对象中获得future对象,仍然需要创建线程来执行这个packaged_task对象,整过过程仍然显得比较繁琐。为了进一步简化这个过程,C++11提供了async()函数,通过简单的一个函数调用,一次性地完成上面这些繁琐的过程。通过async()函数,我们只需要提供一个线程函数,它就会创建并启动相应的分支线程来执行这个线程函数,更关键的是,它还会完成future和promise的配对,并直接返回一个可以获取线程函数返回值的future对象。而主线程只需要通过调用它的get()函数等待分支线程执行结束,就可以直接得到线程函数的返回值。例如,上面的例子可以进一步简化为:

// 将Cook()函数异步(async)执行
future<Food> fu = async(bind(Cook,"回锅肉"));
cout<<"客官,你点的"<<fu.get().GetName()<<"来了,请慢用!"<<endl;

在这里,我们首先利用bind()函数将线程函数Cook()和它的参数共同打包成一个匿名的函数对象,然后作为参数交给async()函数去异步执行。async()函数会根据情况创建新的线程或者是复用已有的线程来执行Cook()线程函数,并返回一个可以在将来(future)取得线程函数返回值的future对象,而通过它的get()函数,主线程就可以在线程函数执行完毕后及时得到它的返回值。

这一路下来,利用future和promise获得线程函数结果数据的方法越来越简单,从最开始的需要十几行代码的通过传递promise对象来完成的方法到现在仅仅需要两行代码的通过async()函数来完成的方法,这就像从专业相机到傻瓜相机的变化,越来越容易使用了。但是,这种变化也是有利也有弊,随着过程的减少,它同样也减少了对整个过程进行更多控制以适应更复杂需求的机会。比如,通过packaged_task的方法,我们无法控制结果数据返回的时机,通过async()函数的方法,我们无法控制线程的执行。这就像傻瓜相机,简单是简单,好用是好用,可就是没法像专业相机那样进行更精细的调节以拍出更精美的照片。而对于专业相机,虽然可以拍出很精彩的照片,却又难以使用。所以,无论是现实生活的相机,还是上面的多线程技术,亦或是我们选择的男女朋友,并没有高低贵贱好坏之分,有的只是合适与不合适的区别。根据我们的具体需求选择最合适的才是最好的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值