天天看點

通俗講解c++ promise/packaged_taskpromise介紹packaged_task介紹

目錄

  • promise介紹
    • 指派
    • 取值
  • packaged_task介紹
    • valid
    • 指派
    • 取值
      • 調用函數(取值準備)
        • 方式一
        • 方式二
    • 狀态延遲
    • 重置狀态

promise介紹

存在于頭檔案 future 中,相當于一個在并行環境中的資料存儲箱,各線程可以通過這一箱子傳遞某種類型的資料

舉個例子

如果我們要定義一個裝int類型的箱子

std::promise<int> boxOfInt
           

指派

  • 如果我們要往箱子裡放東西,比如放一個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;
}
           
  • 更多神奇的指派操作

    傳函數指針、可變元函數

    https://blog.csdn.net/godmaycry/article/details/72844159

取值

要拿出箱子裡的東西

// 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
*/
           

參考

  • https://www.cnblogs.com/haippy/p/3239248.html
  • https://mp.weixin.qq.com/s?src=11&timestamp=1601175837&ver=2609&signature=SGV0wfjTBnqACPlLkyatqBIP2TGLeFoC0QP0UnwMrbjTHkSFD*Rix2vGu4F-WZLkTTOWvF05LT6Hw3tNf9jFn1pMdSbCcs9DvyokI2V0bK1ILpRxIcu3X0CF28r9Vjdi&new=1
  • https://blog.csdn.net/godmaycry/article/details/72844159
  • https://blog.csdn.net/t114211200/article/details/78082703
  • http://www.cplusplus.com/reference/future/promise/
  • http://www.cplusplus.com/reference/future/packaged_task/
  • https://blog.csdn.net/godmaycry/article/details/72868559
  • https://blog.csdn.net/c_base_jin/article/details/79541301?utm_medium=distribute.pc_relevant.none-task-blog-title-1&spm=1001.2101.3001.4242
  • https://blog.csdn.net/nirendao/article/details/52079942
c++

繼續閱讀