多執行緒07:async、future、packaged_task、promise

booker 發表於 2022-05-15

📕async、future、packaged_task、promise

本節內容需要包含標頭檔案:#include <future>

一、std::async、 std::future 建立後臺任務並返回值

①:啟用async

  • std::async是一個函式模板,用來啟動一個非同步任務,啟動起來一個非同步任務之後,它返回一個std::future物件,這個物件是個類别範本。
  • “啟動一個非同步任務”:就是自動建立一個執行緒,並開始 執行對應的執行緒入口函式,它返回一個std::future物件,這個std::future物件中就含有執行緒入口函式所返回的結果,我們可以通過呼叫future物件的成員函式get()來獲取結果。
  • “future”將來的意思,也有人稱呼std::future提供了一種訪問非同步操作結果的機制,就是說這個結果你可能沒辦法馬上拿到,但是在不久的將來,這個執行緒執行完畢的時候,你就能夠拿到結果了,所以,大家這麼理解:future中儲存著一個值,這個值是在將來的某個時刻能夠拿到
  • std::future物件的get()成員函式會等待執行緒執行結束並返回結果,拿不到結果它就會一直等待,感覺有點像join()。但是,它是可以獲取結果的。並且記得一個future只能呼叫一次get(無論哪裡,都會將資料轉移),呼叫兩次程式會崩潰。
  • std::future物件的wait()成員函式,用於等待執行緒返回,本身並不返回結果,這個效果和 std::thread 的join()更像。沒有隻能呼叫一次限制,但是呼叫多也沒有效果。
  • 如果不呼叫future的get()成員函式和wait()成員函式,程式也會在主函式return處一直等待呼叫子執行緒結束;

看例子:

#include <iostream>
#include <future>
using namespace std;
class A {
public:
	int mythread(int data) {
		cout << data << endl;
		return data * 2;
	}
};
 
 
int mythread() {
	cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);//睡5秒
	std::this_thread::sleep_for(dura);
	cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
	return 5;
}
 
 
int main() {
	A a;
	int tmp = 12;
	cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
	std::future<int> result1 = std::async(mythread);
	cout << "continue........" << endl;
	cout << result1.get() << endl; //卡在這裡等待mythread()執行完畢,拿到結果
	
	//類成員函式
	std::future<int> result2 = std::async(&A::mythread, &a, tmp); //第二個引數是物件引用才能保證執行緒裡執行的是同一個物件
	//cout << result2.get() << endl;//get()只能呼叫一次,result2不再有結果值,轉移的是存的資料型別,這裡是int!
   //或者result2.wait();
	cout << "finish all" << endl;
	return 0;
}

②:std::async的第一個引數

  • 引數:std::launch::deferred: 表示執行緒入口函式呼叫被延遲到std::future的wait()或者get()函式呼叫時才執行; 那如果wait()或者get()沒呼叫,那麼執行緒沒執行!執行緒沒有建立!是在主執行緒中呼叫的執行緒入口函式!
#include <iostream>
#include <future>
using namespace std;
 
int mythread() {
	cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);
	std::this_thread::sleep_for(dura);
	cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
	return 5;
}
 
 
int main() {
	cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
	std::future<int> result1 = std::async(std::launch::deferred ,mythread);
	cout << "continue........" << endl;
	cout << result1.get() << endl; //卡在這裡等待mythread()執行完畢,拿到結果
	cout << "finish all" << endl;
	return 0;
}

image-20220418110151080

  • 引數:std::launch::async:在呼叫async函式的時候就開始建立執行緒;會強制std::async建立新執行緒,和thread一樣。
  • 帶上兩個引數:std::launch::async |std::launch::deferred 這裡這個 |:以為這呼叫async的行為可能是 建立新執行緒並立即執行,或者沒有建立新執行緒並且延遲呼叫result.get()才開始執行任務入口函式,兩者居其一。(系統會自行決定是非同步(建立新執行緒)還是同步(不建立新執行緒)方式執行),預設值就是這個!

二、std::package_task

  • 目的就是:打包任務,把任務包裝起來
  • std::packaged_task是個模板類,它的模板引數是各種可呼叫物件(函式,函式指標,lambda表示式,bind建立的物件,以及過載了函式呼叫符的類,有點像function);通過std::packaged_task來把各種可呼叫物件包裝起來,方便將來作為執行緒入口函式來呼叫。
  • packaged_task包裝起來的可呼叫物件還可以直接呼叫,所以從這個角度來講,packaged_task物件,也是一個可呼叫物件。
  • 看例子:
//可呼叫物件是普通函式
#include <thread>
#include <iostream>
#include <future>
using namespace std;
 
int mythread(int mypar) {
	cout << mypar << endl;
	cout << "mythread() start" << "threadid = " << std::this_thread::get_id() << endl;
	std::chrono::milliseconds dura(5000);
	std::this_thread::sleep_for(dura);
	cout << "mythread() end" << "threadid = " << std::this_thread::get_id() << endl;
	return 5;
}
 
int main() {
	cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
	//我們把函式mythread通過packaged_task包裝起來
    //引數是一個int,返回值型別是int
    std::packaged_task<int(int)> mypt(mythread);
	std::thread t1(std::ref(mypt), 1);//傳入真引用!不可以用detach模式
	t1.join();
	std::future<int> result = mypt.get_future(); 
	//std::future物件裡包含有執行緒入口函式的返回結果,這裡result儲存mythread返回的結果。
	cout << result.get() << endl;
   
	return 0;
}

//可呼叫物件是lambda表示式
int main() {
	cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
    
	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
		return 5;
	}); 
	
	std::thread t1(std::ref(mypt), 1);//傳真引用,因為是packege_task,mypt只能再被呼叫一次
	t1.join();
	std::future<int> result = mypt.get_future(); 
	
	cout << result.get() << endl;
 
	cout << "finish overhead" << endl;
	return 0;
}

image-20220418112915922

//呼叫物件是lambda表示式,直接呼叫
int main() {
	cout << "main" << "threadid = " << std::this_thread::get_id() << endl;
    
	std::packaged_task<int(int)> mypt([](int mypar) {
		cout << mypar << endl;
		cout << "mythread() start" << " threadid = " << std::this_thread::get_id() << endl;
		std::chrono::milliseconds dura(5000);
		std::this_thread::sleep_for(dura);
		cout << "mythread() end" << " threadid = " << std::this_thread::get_id() << endl;
		return 5;
	}); 
    
	mypt(1);//也可以在主執行緒中直接呼叫,這樣就和function的效果一樣,但是隻能呼叫一次
	std::future<int> result = mypt.get_future(); 
	cout << result.get() << endl;
 
	cout << "finish overhead" << endl;
	return 0;
}

image-20220418113609731

tip:瞭解ref

三、promise

  • std::promise 類别範本,我們能夠在某個執行緒中給它賦值,然後我們可以在其他執行緒中把這個值取出來用;

  • 總結:通過promise儲存一個值,在將來某時刻我們通過把一個future繫結到這個promise上來得到這個繫結的值;

#include<iostream>
#include<thread>
#include<mutex>
#include<future>
 
using namespace std;
 
void mythread(std::promise<int>&tmpp, int calc)
{
	//做一系列複雜的操作
	calc++;
	calc *= 10;
 
	//做其他運算,比如整整花費了5秒鐘
	std::chrono::milliseconds dura(5000); //定一個5秒的時間
	std::this_thread::sleep_for(dura);  //休息一定時常
 
	int result = calc; //儲存結果
	tmpp.set_value(result);  //####1.結果儲存到了tmpp這個promise物件中
} 
 
void mythread2(std::future<int> &tmpf) 
{
	auto result = tmpf.get();
	cout <<"mythread result = " << result<<endl;
}
 
int main()
{
	std::promise<int> myprom; //宣告一個std::promise物件myprom,儲存的值型別為int;
	std::thread t1(mythread,std::ref(myprom),180);
	t1.join();
 
	//獲取結果值
	std::future<int> fu1 = myprom.get_future();//####2.promise與future繫結,用於獲取執行緒返回值
 
	std::thread t2(mythread2,std::ref(fu1));
	t2.join(); //等mythread2執行完畢
 
	cout << "finish all"  << endl;
 	
    //fu1不再有用,已經為空;已經全部傳入mythread2的tmpf中,但是myprom還存著值1810;
	return 0;
}

//對於ref,package_task、future都會移動後原本的變數就沒有內容了,對於promise,卻還有,要探索一下

再瞭解async\future\等

四、std::asyn深入理解

4.1 std::async引數詳述

  • 延遲呼叫引數 std::launch::deferred【延遲呼叫】,std::launch::async【強制建立一個執行緒】;

  • std::async()我們一般不叫建立執行緒(他能夠建立執行緒),我們一般叫它建立一個非同步任務。std::async和std::thread最明顯的不同:就是 async 有時候並不建立新執行緒

①如果用std::launch::deferred 來呼叫async?

延遲到呼叫 get() 或者 wait() 時執行,如果不呼叫就不會執行

②如果用std::launch::async來呼叫async?

強制這個非同步任務在新執行緒上執行,這意味著,系統必須要建立出新執行緒來執行入口函式。

③如果同時用 std::launch::async | std::launch::deferred

這裡這個 | 意味著async的行為可能是 std::launch::async 建立新執行緒立即執行, 也可能是 std::launch::deferred 沒有建立新執行緒並且延遲到呼叫get()執行,由系統根據實際情況來決定採取哪種方案

④不帶額外引數 std::async(mythread),只給async 一個入口函式名,此時的系統給的預設值是 std::launch::async | std::launch::deferred 和 ③ 一樣,有系統自行決定非同步還是同步執行。

4.2 std::async和std::thread()區別

  • std::thread()如果系統資源緊張可能出現建立執行緒失敗的情況,如果建立執行緒失敗那麼程式就可能崩潰,而且不容易拿到函式返回值(不是拿不到)
  • std::async()建立非同步任務。可能建立執行緒也可能不建立執行緒,並且容易拿到執行緒入口函式的返回值;

由於系統資源限制:
①如果用std::thread建立的執行緒太多,則可能建立失敗,系統報告異常,崩潰。

②如果用std::async,一般就不會報異常,因為如果系統資源緊張,無法建立新執行緒的時候,async不加額外引數的呼叫方式就不會建立新執行緒。而是在後續呼叫get()請求結果時執行在這個呼叫get()的執行緒上。

如果你強制async一定要建立新執行緒就要使用 std::launch::async 標記。承受的代價是,系統資源緊張時可能崩潰。

③根據經驗,一個程式中執行緒數量 不宜超過100~200

4.3 async不確定性問題的解決

讓系統自行決定是否建立執行緒:std::future<int> result = std::async(mythread);問題焦點在於這個寫法,任務到底有沒有被推遲執行!

如何判斷:通過future中wait_for()方法的返回值;詳情看下一節內容

多閱讀高手程式碼,多積累,提升自己技術!