《C++ Concurrency In Action》part5 同步並行作業(下)
4.2 使用期望等待一次性事件
期望 (future):當一個線程需要等待一個特定的一次性事件時,在某種程度上來說它就需要知道這個事件在未來的表現形式。之後,這個線程會周期性(較短的周期)的等待或檢查,事件是否觸發(檢查資訊板);在檢查期間也會執行其他任務。
另外,在等待任務期間它可以先執行另外一些任務,直到對應的任務觸發,而後等待期望的狀態會變為“就緒”(ready)。一個“期望”可能是資料相關的,也可能不是。當事件發生時(並且期望狀態為就緒),這個“期望”就不能被重設。
在C++標準庫中,有兩種“期望”,使用兩種類型模板實現,聲明在標頭檔中: 唯一期望(unique futures)( std::future<> )和共用期望(shared futures)( std::shared_future<> )。這是仿照 std::unique_ptr 和 std::shared_ptr 。 std::future 的執行個體只能與一個指定事件相關聯,而 std::shared_future 的執行個體就能關聯多個事件。後者的實現中,所有執行個體會在同時變為就緒狀態,並且他們可以訪問與事件相關的任何資料。這種資料關聯與模板有關,比如 std::unique_ptr 和 std::shared_ptr 的模板參數就是相關聯的資料類型。在與資料無關的地方,可以使用 std::future 與 std::shared_future 的特化模板。雖然,我希望用於線程間的通訊,但是“期望”對象本身並不提供同步訪問。當多個線程需要訪問一個獨立“期望”對象時,他們必須使用互斥量或類似同步機制對訪問進行保護。 4.2.1 帶傳回值的背景工作
假設,你有一個需要長時間的運算,你需要其能計算出一個有效值,但是你現在並不迫切需要這個值。你可以啟動一個新線程來執行這個計算,但是這就意味著你必須關注如何傳回計算的結果,因為 std::thread 並不提供直接接收傳回值的機制。這裡就需要 std::async 函數模板(也是在標頭檔中聲明的)了。
當任務的結果你不著急要時,你可以使用 std::async 啟動一個非同步任務。與 std::thread 對象等待運行方式的不同, std::async 會返回一個 std::future 對象,這個對象持有最終計算出來的結果。當你需要這個值時,你只需要調用這個對象的get()成員函數;並且直到“期望”狀態為就緒的情況下,線程才會阻塞;之後,返回計算結果。
例:使用 std::future 從非同步任務中擷取傳回值
#include <future>#include <iostream>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::thread 做的方式一樣, std::async 允許你通過添加額外的調用參數,向函數傳遞額外的參數。當第一個參數是一個指向成員函數的指標,第二個參數提供有這個函數成員類的具體對象(不是直接的,就是通過指標,還可以封裝在 std::ref 中),剩餘的參數可作為成員函數的參數傳入。否則,第二個和隨後的參數將作為函數的參數,或作為指定可調用對象的第一個參數。就如 std::thread ,當參數為右值(rvalues)時,拷貝操作將使用移動的方式轉移未經處理資料。這就允許使用“只移動”類型作為函數對象和參數。
例:使用 std::async 向函數傳遞參數
#include <string>#include <future>struct X{void foo(int,std::string const&);std::string bar(std::string const&);};X x;auto f1=std::async(&X::foo,&x,42,"hello"); // 調用p->foo(42, "hello"),p是指向x的指標auto f2=std::async(&X::bar,x,"goodbye"); // 調用tmpx.bar("goodbye"), tmpx是x的拷貝副本struct Y{double operator()(double);};Y y;auto f3=std::async(Y(),3.141); // 調用tmpy(3.141),tmpy通過Y的移動建構函式得到auto f4=std::async(std::ref(y),2.718); // 調用y(2.718)X baz(X&);std::async(baz,std::ref(x)); // 調用baz(x)class move_only{public:move_only();move_only(move_only&&)move_only(move_only const&) = delete;move_only& operator=(move_only&&);move_only& operator=(move_only const&) = delete;void operator()();};auto f5=std::async(move_only()); // 調用tmp(),tmp是通過std::move(move_only())構造得到
在預設情況下,這取決於 std::async 是否啟動一個線程,或是否在期望等待時同步任務。在大多數情況下(估計這就是你想要的結果),但是你也可以在函數調用之前,向 std::async 傳遞一個額外參數。這個參數的類型是 std::launch ,還可以是 std::launch::defered ,用來表明函數調用被延遲到wait()或get()函數調用時才執行, std::launch::async 表明函數必須在其所在的獨立線程上執行, std::launch::deferred | std::launch::async 表明實現可以選擇這兩種方式的一種。最後一個選項是預設的。當函數調用被延遲,它可能不會再運行了。
如:
auto f6=std::async(std::launch::async,Y(),1.2); // 在新線程上執行auto f7=std::async(std::launch::deferred,baz,std::ref(x)); // 在wait()或get()調用時執行auto f8=std::async(std::launch::deferred | std::launch::async,baz,std::ref(x)); // 實現選擇執行方式auto f9=std::async(baz,std::ref(x));f7.wait(); // 調用延遲函數
使用 std::async 會讓分割演算法到各個任務中變的容易,這樣程式就能並發的執行了。不過,這不是讓一個 std::future 與一個任務執行個體相關聯的唯一方式;你也可以將任務封裝入一個 std::packaged_task<> 執行個體中,或通過編寫代碼的方式,使用 std::promise<> 類型模板顯示設定值。與 std::promise<> 對比, std::packaged_task<> 具有更高層的抽象,所以我們從“高抽象”的模板說起。 4.2.2 任務與期望