通俗讲解c++ promise/packaged_task

promise介绍

存在于头文件 future 中,相当于一个在并行环境中的数据存储箱,各线程可以通过这一箱子传递某种类型的数据

举个例子
如果我们要定义一个装int类型的箱子

std::promise<int> boxOfInt

赋值

  • 如果我们要往箱子里放东西,比如放一个10
boxOfInt.set_value(10)

箱子同时只能放一个东西,如果还想多放(多次赋值),就会报错,terminate called after throwing an instance of ‘std::future_error’ what(): std::future_error: Promise already satisfied terminate called recursively

放好东西后,箱子就会给自己贴一个标签(共享状态标志变为ready)
这个标签,也可以放的线程结束后再贴

如果我们把东西放进箱子后,

  • 并不想马上就把箱子关上(即马上把共享标志变为ready),可以使用
    set_value_at_thread_exit(),正如其字面意思,在线程结束后设值
    void f(promise<int> &p)
    {
        p.set_value_at_thread_exit(123);//可以正常使用
    }
    void main()
    {
        promise<int> p;
        auto re = p.get_future();
        thread t(f, std::ref(p));
        t.join();
        cout << re.get() << endl;
 
        system("pause");
    }

注意:如果要使用set_value_at_thread_exit函数,要保证在线程退出时,promise对象不能被销毁

什么场景下会用 std::promise::set_value_at_thread_exit?

想象一下,如果有一个人一直在等另一个人往箱子里放东西,等箱子里有东西之后,它拿到东西就准备过河,但过河的桥还没修好,东西却已经准备好了。如果直接把东西放进箱子,它拿到东西,但桥都没有,跑过去只能掉河里了。
set_value_at_thread_exit就是为了解决这个矛盾,在资源没有准备好的情况下,不告诉别人已经放好东西了(用来阻塞另外一个线程,实现对线程执行顺序和共享资源的控制)

  • 当然除了放某种类型的值之外,也可以在里面放异常对象
boxOfException.set_exception(std::current_exception());

同理,也有set_exception_at_thread_exit和set_value_at_thread_exit类似
举例说明

#include <iostream>       // std::cin, std::cout, std::ios
#include <functional>     // std::ref
#include <thread>         // std::thread
#include <future>         // std::promise, std::future
#include <exception>      // std::exception, std::current_exception

// 函数的目的是 获取用户输入的数据
void get_int(std::promise<int>& prom) {
    int x;
    std::cout << "Please, enter an integer value: ";
    std::cin.exceptions (std::ios::failbit);  // throw on failbit
    try {
        std::cin >> x;  // sets failbit if input is not int
        prom.set_value(x);
    } catch (std::exception&) {
        // 当输入的数据不是int类型的,无法装入箱子时,就在里面装一个异常
        prom.set_exception(std::current_exception());
    }
}

void print_int(std::future<int>& fut) {
    try {
        int x = fut.get();
        std::cout << "value: " << x << '\n';
    } catch (std::exception& e) {
        // 发现箱子里装进了一个异常,而不是一个数据,输出
        // [exception caught: basic_ios::clear: iostream error]
        std::cout << "[exception caught: " << e.what() << "]\n";
    }
}

int main ()
{
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();

    std::thread th1(get_int, std::ref(prom));
    std::thread th2(print_int, std::ref(fut));

    th1.join();
    th2.join();
    return 0;
}

取值

要拿出箱子里的东西

// future之后会讲解
std::future<int> fut = boxOfInt.get_future();
int something = fut.get();

注意了,promise用完一次后,就无法继续使用(get 只能调用一次),如果继续使用会出现,terminate called after throwing an instance of ‘std::future_error’  what(): std::future_error: Future already retrieved

可以想成成程序每次打开promise都是暴力拆箱,拆完箱子就烂了,只能重新给它个新的。
( 箱子是独一无二的,std::promise的普通赋值操作是被禁用的,禁止拷贝)

boxOfInt = std::promise<int>();  // 赋值为一个新的promise对象.
  • 如果箱子里没有放东西,那就会拿不出来,程序就会阻塞,等待箱子里有东西可以拿。

  • 同样箱子里的东西,如果已经被拿走了,下一个来拿的程序,自然就会什么都拿不到。

下面是一串可以正常运行的代码

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <future>         // std::promise, std::future

// 创建一个装int类型的箱子
std::promise<int> boxOfInt;

// 这个函数目的就是从箱子里取出东西
void print_global_promise () {
    std::future<int> fut = boxOfInt.get_future();
    int x = fut.get();
    std::cout << "value: " << x << '\n';
}

int main ()
{
    std::thread th1(print_global_promise);
    // 往箱子里放数据,th1线程就会去拿数据
    boxOfInt.set_value(10); 
    th1.join();

    // 箱子被th1弄烂了,只好重新弄一个
    boxOfInt = std::promise<int>();
    
    // 同理 th2 也来开箱子
    std::thread th2 (print_global_promise);
    boxOfInt.set_value (20);
    th2.join();

    return 0;
}

可以自己尝试注释掉,放东西(set_value),做新箱子等步骤,查看报错的信息。

packaged_task介绍

packaged_task 和 promise 比较类似,只是promise是可以装多种类型的箱子。
而packaged_task 更像是一种promise的一种特化,它是一个专门装函数的箱子,使得函数的调用更加便捷

如果一个promise要装函数

// 定义一个函数
int Test_Fun(int a, int b, int &c)
{
	//a = 1, b = 2
	c = a + b + 230;
	return c;
}

// 声明一个可调对象F
// 等同于typedef std::function<int(int, int, int&)> F;
using F = std::function<int(int, int, int&)>;		
std::promise<F> pr1;

// 传入函数Test_Fun
pr1.set_value(std::bind(&Test_Fun, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

// 取出函数
std::future<F> f = pr1.get_future();
auto fun = f.get();		//fun等同于Test_Fun,只是被包装成了lambda函数

// 如果要执行这个函数
int val = 3;
auto result = fun(1,2,val);

可以看出在整个的 装入-取出-执行 的过程还是比较繁琐的

valid

作用是检查package是否和一个有效的共享状态相关联,

可以理解为查看箱子里面是否有东西存在,该成员函数在promise中是不存在的

使用默认构造函数生成的package_task对象,使用valid返回的是一个false
只有使用move赋值,或者swap操作后才会变为true

// packaged_task::get_future
#include <iostream>     // std::cout
#include <utility>      // std::move
#include <future>       // std::packaged_task, std::future
#include <thread>       // std::thread

// function that launches an int(int) packaged_task in a new thread:
std::future<int> launcher (std::packaged_task<int(int)>& tsk, int arg) {
	if (tsk.valid()) {
      std::future<int> ret = tsk.get_future();
	  std::thread (std::move(tsk),arg).detach();
	  return ret;
	}
	else return std::future<int>();
}

int main ()
{
  std::packaged_task<int(int)> tsk ([](int x){return x*2;});

  std::future<int> fut = launcher (tsk,25);

  std::cout << "The double of 25 is " << fut.get() << ".\n";

  return 0;
}

赋值

而如果要在packaged_task中放函数

// 定义一个函数
int countdown (int from, int to) {
    for (int i=from; i!=to; --i) {
        std::cout << i << '\n';
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    std::cout << "Finished!\n";
    return from - to;
}

// 向packaged_task中放入函数
std::packaged_task<int(int,int)> task(countdown); // 设置 packaged_task

// 也可以使用 lambda 表达式初始化一个 packaged_task 对象.
std::packaged_task<int(int)> bar([](int x){return x*2;});

// 使用move-赋值
std::packaged_task<int(int)> foo; // 默认构造函数.
foo = std::move(bar); // move-赋值操作,也是 C++11 中的新特性.

package_task 和 promise 都是只能使用 move赋值 的,禁止使用 赋值操作 运算

取值

调用函数(取值准备)

package_task的结果和promise一样,也是通过future返回的,
由于package_task里面装的是一个可调用对象,
如果不调用它,是不会有结果返回来的。

std::package_task 不会自己启动,必须我们自己去调用它。

方式一
// 调用方式1,使用thead启动
#include <iostream>
#include <future>
#include <chrono>
#include <functional>
 
int Test_Fun(int a, int b, int &c)
{
	//a=1,b=2,c=0
 
	//突出效果,休眠5s
	std::this_thread::sleep_for(std::chrono::seconds(5));
 
	//c=233
	c = a + b + 230;
 
	return c;
}
 
int main()
{
	//声明一个std::packaged_task对象pt1,包装函数Test_Fun
	std::packaged_task<int(int, int, int&)> pt1(Test_Fun);
	
	//声明一个std::future对象fu1,包装Test_Fun的返回结果类型,并与pt1关联
	// 注意这里的future里面只有一个int,因为future只包含返回值
	std::future<int> fu1 = pt1.get_future();
 
	//声明一个变量
	int c = 0;
 
	//创建一个线程t1,将pt1及对应的参数放到线程里面执行
	// 通过 thread 调用函数
	std::thread t1(std::move(pt1), 1, 2, std::ref(c));
	
	//阻塞至线程t1结束(函数Test_Fun返回结果)
	int iResult = fu1.get();
 
	std::cout << "执行结果:" << iResult << std::endl;	//执行结果:233
	std::cout << "c:" << c << std::endl;	//c:233
 
	return 1;
}
方式二
// 调用方式二,使用仿函数启动
#include <iostream>           // std::cout
#include <thread>             // std::thread
#include <chrono>
#include <future>

using namespace std;
//普通函数
int Add(int x, int y)
{
    return x + y;
}


void task_lambda()
{
    //包装可调用目标时lambda
    packaged_task<int(int,int)> task([](int a, int b){ return a + b;});
    
    //仿函数形式,启动任务
    task(2, 10);
    
    //获取共享状态中的值,直到ready才能返回结果或者异常
    future<int> result = task.get_future();
    cout << "task_lambda :" << result.get() << "\n";
}

void task_thread()
{
    //包装普通函数
    std::packaged_task<int (int,int)> task(Add);
    future<int> result = task.get_future();
    //启动任务,非异步
    task(4,8);
    cout << "task_thread :" << result.get() << "\n";
        
    //重置共享状态
    task.reset();
    result = task.get_future();

    //通过线程启动任务,异步启动
    thread td(move(task), 2, 10);
    td.join();
    //获取执行结果
    cout << "task_thread :" << result.get() << "\n";
}

int main(int argc, char *argv[])
{
    task_lambda();
    task_thread();

    return 0;
}
//函数
int Test_Fun(int iVal)
{
    std::cout << "Value is:" << iVal << std::endl;
    return iVal + 232;
}
 
//声明一个std::packaged_task对象pt1,包装函数Test_Fun
std::packaged_task<int(int)> pt1(Test_Fun);
//声明一个std::future对象,包装Test_Fun的返回结果,并与pt1关联
std::future<int> fu1 = pt1.get_future();

// 这里添加一种启动方式
// pt1(1);
// std::thread t1(std::move(pt1),1); t1.join();

int result = fu1.get()

注意:使用std::packaged_task关联的std::future对象保存的数据类型是可调对象的返回结果类型,
如示例函数的返回结果类型是int,那么声明为std::future< int >,而不是std::future< int (int) >

状态延迟

在promise中,如果想在线程结束后,才把共享状态置为ready,
使用的是set_value_at_thread_exit/set_exception_at_thread_exit

在packaged_task中使用的是make_ready_at_thread_exit

// Not supported by g++ 4.9.3 or Visual Studio 2013

#include <future>
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <utility>
 
void worker(std::future<void>& output)
{
    std::packaged_task<void(bool&)> my_task{ [](bool& done) { done=true; } };
 
    auto result = my_task.get_future();
 
    bool done = false;
 
    my_task.make_ready_at_thread_exit(done); // execute task right away
 
    std::cout << "worker: done = " << std::boolalpha << done << std::endl;
 
    auto status = result.wait_for(std::chrono::seconds(0));
    if (status == std::future_status::timeout)
        std::cout << "worker: result is not ready yet" << std::endl;
 
    output = std::move(result);
}
 
 
int main()
{
    std::future<void> result;
 
    std::thread{worker, std::ref(result)}.join();
 
    auto status = result.wait_for(std::chrono::seconds(0));
    if (status == std::future_status::ready)
        std::cout << "main: result is ready" << std::endl;
}

重置状态

promise 在使用完一次后(即成功取值后),就无法继续使用了
而packaged_task,在使用过一次后,可以通过reset函数重获新生

#include <iostream>
#include <cmath>
#include <thread>
#include <future>
 
int main()
{
    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 << "2^9 = " << result.get() << '\n';
 
    task.reset();
    result = task.get_future();
    std::thread task_td(std::move(task), 2, 10);
    task_td.join();
    std::cout << "2^10 = " << result.get() << '\n';
}

/* Output:
2^9 = 512
2^10 = 1024
*/

参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值