(原創)用C++11的std::async代替線程的建立

(原創)用C++11的std::async代替線程的建立

  c++11中增長了線程,使得咱們能夠很是方便的建立線程,它的基本用法是這樣的:html

void f(int n);
std::thread t(f, n + 1);
t.join();

  可是線程畢竟是屬於比較低層次的東西,有時候使用有些不便,好比我但願獲取線程函數的返回結果的時候,我就不能直接經過thread.join()獲得結果,這時就必須定義一個變量,在線程函數中去給這個變量賦值,而後join,最後獲得結果,這個過程是比較繁瑣的。c++11還提供了異步接口std::async,經過這個異步接口能夠很方便的獲取線程函數的執行結果。std::async會自動建立一個線程去調用線程函數,它返回一個std::future,這個future中存儲了線程函數返回的結果,當咱們須要線程函數的結果時,直接從future中獲取,很是方便。可是我想說的是,其實std::async給咱們提供的便利可不只僅是這一點,它首先解耦了線程的建立和執行,使得咱們能夠在須要的時候獲取異步操做的結果;其次它還提供了線程的建立策略(好比能夠經過延遲加載的方式去建立線程),使得咱們能夠以多種方式去建立線程。在介紹async具體用法以及爲何要用std::async代替線程的建立以前,我想先說一說std::future、std::promise和std::packaged_task。c++

std::future

  std::future是一個很是有用也頗有意思的東西,簡單說std::future提供了一種訪問異步操做結果的機制。從字面意思來理解,它表示將來,我以爲這個名字很是貼切,由於一個異步操做咱們是不可能立刻就獲取操做結果的,只能在將來某個時候獲取,可是咱們能夠以同步等待的方式來獲取結果,能夠經過查詢future的狀態(future_status)來獲取異步操做的結果。future_status有三種狀態:express

  • deferred:異步操做還沒開始
  • ready:異步操做已經完成
  • timeout:異步操做超時
 
//查詢future的狀態
std::future_status status;
    do {
        status = future.wait_for(std::chrono::seconds(1));
        if (status == std::future_status::deferred) {
            std::cout << "deferred\n";
        } else if (status == std::future_status::timeout) {
            std::cout << "timeout\n";
        } else if (status == std::future_status::ready) {
            std::cout << "ready!\n";
        }
    } while (status != std::future_status::ready); 
 

  獲取future結果有三種方式:get、wait、wait_for,其中get等待異步操做結束並返回結果,wait只是等待異步操做完成,沒有返回值,wait_for是超時等待返回結果。編程

std::promise

  std::promise爲獲取線程函數中的某個值提供便利,在線程函數中給外面傳進來的promise賦值,當線程函數執行完成以後就能夠經過promis獲取該值了,值得注意的是取值是間接的經過promise內部提供的future來獲取的。它的基本用法:promise

    std::promise<int> pr;
    std::thread t([](std::promise<int>& p){ p.set_value_at_thread_exit(9); },std::ref(pr));
    std::future<int> f = pr.get_future();
    auto r = f.get();

std::packaged_task

  std::packaged_task它包裝了一個可調用的目標(如function, lambda expression, bind expression, or another function object),以便異步調用,它和promise在某種程度上有點像,promise保存了一個共享狀態的值,而packaged_task保存的是一個函數。它的基本用法:markdown

    std::packaged_task<int()> task([](){ return 7; });
    std::thread t1(std::ref(task)); 
    std::future<int> f1 = task.get_future(); 
    auto r1 = f1.get();

std::promise、std::packaged_task和std::future的關係

  至此, 咱們介紹了std::async相關的幾個對象std::future、std::promise和std::packaged_task,其中std::promise和std::packaged_task的結果最終都是經過其內部的future返回出來的,不知道讀者有沒有搞糊塗,爲何有這麼多東西出來,他們之間的關係究竟是怎樣的?且聽我慢慢道來,std::future提供了一個訪問異步操做結果的機制,它和線程是一個級別的屬於低層次的對象,在它之上高一層的是std::packaged_task和std::promise,他們內部都有future以便訪問異步操做結果,std::packaged_task包裝的是一個異步操做,而std::promise包裝的是一個值,都是爲了方便異步操做的,由於有時我須要獲取線程中的某個值,這時就用std::promise,而有時我須要獲一個異步操做的返回值,這時就用std::packaged_task。那std::promise和std::packaged_task之間又是什麼關係呢?說他們不要緊也關係,說他們有關係也有關係,都取決於你了,由於我能夠將一個異步操做的結果保存到std::promise中。若是讀者還沒搞清楚他們的關係的話,我就用更通俗的話來解釋一下。好比,一個小夥子給一個姑娘表白真心的時候也許會說:」我許諾會給你一個美好的將來「或者」我會努力奮鬥爲你創造一個美好的將來「。姑娘每每會說:」我等着「。如今我來將這三句話用c++11來翻譯一下:併發

小夥子說:我許諾會給你一個美好的將來等於c++11中"std::promise a std::future"; 
小夥子說:我會努力奮鬥爲你創造一個美好的將來等於c++11中"std::packaged_task a future"; 
姑娘說:我等着等於c++11中"future.get()/wait()";異步

  小夥子兩句話的箇中差別,本身琢磨一下,這點差別也是std::promise和std::packaged_task的差別。現實中的山盟海誓靠不靠得住我不知道,可是c++11中的許諾和將來是必定可靠的,發起來了許諾就必定有將來。細想起來c++11標準的制定者選定的關鍵字真是貼切而有意思!好了,插科打諢到此了,如今言歸正傳,回過頭來講說std::async。async

爲何要用std::async代替線程的建立

  std::async又是幹啥的,已經有了td::future、std::promise和std::packaged_task,夠多的了,真的還要一個std::async來湊熱鬧嗎,std::async表示很委屈:我不是來湊熱鬧的,我是來幫忙的。是的,std::async是爲了讓用戶的少費點腦子的,它讓這三個對象默契的工做。大概的工做過程是這樣的:std::async先將異步操做用std::packaged_task包裝起來,而後將異步操做的結果放到std::promise中,這個過程就是創造將來的過程。外面再經過future.get/wait來獲取這個將來的結果,怎麼樣,std::async真的是來幫忙的吧,你不用再想到底該怎麼用std::future、std::promise和std::packaged_task了,std::async已經幫你搞定一切了!函數

  如今來看看std::async的原型async(std::launch::async | std::launch::deferred, f, args...),第一個參數是線程的建立策略,有兩種策略,默認的策略是當即建立線程:

  • std::launch::async:在調用async就開始建立線程。
  • std::launch::deferred:延遲加載方式建立線程。調用async時不建立線程,直到調用了future的get或者wait時才建立線程。

第二個參數是線程函數,第三個參數是線程函數的參數。


std::async基本用法:

 
std::future<int> f1 = std::async(std::launch::async, [](){ 
        return 8;  
    }); 

cout<<f1.get()<<endl; //output: 8

std::future<int> f2 = std::async(std::launch::async, [](){ 
        cout<<8<<endl;
    }); 

f2.wait(); //output: 8

std::future<int> future = std::async(std::launch::async, [](){ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        return 8;  
    }); 
 
    std::cout << "waiting...\n";
    std::future_status status;
    do {
        status = future.wait_for(std::chrono::seconds(1));
        if (status == std::future_status::deferred) {
            std::cout << "deferred\n";
        } else if (status == std::future_status::timeout) {
            std::cout << "timeout\n";
        } else if (status == std::future_status::ready) {
            std::cout << "ready!\n";
        }
    } while (status != std::future_status::ready); 
 
    std::cout << "result is " << future.get() << '\n';
可能的結果:
waiting...
timeout
timeout
ready!
result is 8
 

總結:

  std::async是更高層次上的異步操做,使咱們不用關注線程建立內部細節,就能方便的獲取異步執行狀態和結果,還能夠指定線程建立策略,應該用std::async替代線程的建立,讓它成爲咱們作異步操做的首選。

 

若是你以爲這篇文章對你有用,能夠點一下推薦,謝謝。

c++11 boost技術交流羣:296561497,歡迎你們來交流技術。

一點夢想:盡本身一份力,讓c++的世界變得更美好!
 
 

Effective Modern C++ 條款36 若是異步執行是必需的,指定std::launch::async策略

若是異步執行是必需的,指定std::launch::async策略

當你調用std::async來執行一個函數(或一個可執行對象)時,你一般但願函數是異步執行的。但你沒有要求std::async必須這樣作,函數是根據std::async的發射策略(launch policy)來執行的。有兩個標準策略,每一個都是經過std::launch局部枚舉(scoped enum, 看條款10)來表示。假設一個函數f要傳遞給std::launch執行,

  • std::launch::async發射策略意味着函數f必須異步執行,即在另外一線程執行。
  • std::launch::deferred發射策略意味着函數f可能只會在——std::async返回的future對象調用get或wait時——執行。那就是,執行會推遲到其中一個調用發生。當調用get或wait時,f會同步執行,即,調用者會阻塞直到f運行結束。若是get或wait沒有被調用,f就絕對不會執行。

可能很奇怪,std::async的默認發射策略——它的默認策略是你不能顯式指定的——不是二者其中的一種,相反,是二者進行或運算。下面兩個函數徹底是相同的意思:

auto fut1 = std::async(f); // 使用默認發射策略執行f auto fut2 = std::async(std::launch::async | // 使用async或deferred執行f std::launch::deferred f);

默認的發射策略容許異步或同步執行函數f,就如條款35指出,這個靈活性讓std::async與標準庫的線程管理組件一塊兒承擔線程建立和銷燬、避免過載、負責均衡的責任。這讓用std::async進行併發編程變得很方便。

但用std::async的默認發射策略會有一些有趣的含義。這語句給定一個線程t執行f

auto fut = std::async(f); // 使用默認發射模式執行f
  • 沒有辦法預知函數f是否會和線程t併發執行,由於f可能會被調度爲推遲執行。
  • 沒有辦法預知函數f是否運行在——與調用get或wait函數的線程不一樣的——線程。若是那個線程是t,這句話的含義是沒有辦法預知f是否會運行在與t不一樣的線程。
  • 可能沒有辦法預知函數f是否執行徹底,由於沒有辦法保證fut會調用get或wait。 

默認發射策略的調度靈活性常常會混淆使用thread_local變量,這意味着若是f寫或讀這種線程本地存儲(Thread Local Storage,TLS),預知取到哪一個線程的本地變量是不可能的:

auto fut = std::async(f); // f使用的線程本地存儲變量多是獨立的線程的, // 也多是fut調用get或wait的線程的

 

它也影響了基於wait循環中的超時狀況,由於對一個推遲(策略爲deferred)的任務(看條款35)調用wait_for或者wait_until會返回值std::launch::deferred。這意味着下面的循環,看起來最終會中止,可是,實際上可能會一直運行:

using namespace std::literals; // 對於C++14的持續時間後綴,請看條款34 void f() // f睡眠1秒後返回 { std::this_thread::sleep_for(1s); } auto fut = std::async(f); // (概念上)異步執行f while(fut.wait_for(100ms) != // 循環直到f執行結束 std::future_status::ready) // 但這可能永遠不會發生 { ... }

若是f與調用std::async的線程併發執行(即,使用std::launch::async發射策略),這裏就沒有問題(假設f能結束執行,不會一直死循環)。但若是f被推遲(deferred),fut.wait_for將老是返回std::future_status::deferred。那永遠也不會等於std::future_status::ready,因此循環永遠不會終止。

這種bug在開發或單元測試中很容易被忽略,由於它只會在機器負載很重時纔會顯現。在機器過載(oversubscription)或線程消耗完的情況下,任務極可能會被推遲(若是使用的是默認發射策略)。總之,若是不是過載或者線程耗盡,運行系統沒有理由不調度任務併發執行。

解決辦法很簡單:檢查std::async返回的future,看它是否把任務推遲,而後呢,若是真的是那樣,就避免進入基於超時的循環。不幸的是,沒有辦法直接詢問future的任務是否被推遲。取而代之的是,你必須調用一個基於超時的函數——例如wait_for函數。在這種狀況下,你不用等待任何事情,你只是要看看返回值是否爲std::future_status::deferred,因此請相信這迂迴的話語和用0來調用wait_for:

auto fut = std::async(f); // 如前 if (fut.wait_for(0) == std::future_status::deferred) // 若是任務被推遲 { ... // fut使用get或wait來同步調用f } else { // 任務沒有被推遲 while(fut.wait_for(100ms) != std::future_status::ready) { // 不可能無限循環(假定f會結束) ... // 任務沒有被推遲也沒有就緒,因此作一些併發的事情直到任務就緒 } ... // fut就緒 }

 

考慮多種因素的結論是,只要知足了下面的條件,以默認發射策略對任務使用std::async能正常工做:

  • 任務不須要與調用get或wait的線程併發執行。
  • 修改哪一個線程的thread_local變量都不要緊。
  • 要麼保證std::async返回的future會調用get或wait,要麼你能接受任務可能永遠都不執行。
  • 使用wait_for或wait_unitil的代碼要考慮到任務推遲的可能性。

若是其中一個條件沒有知足,你極可能是想要確保任務能異步執行。而那樣作的方法是,當你調用std::async時,把std::launch::async做爲第一個參數傳遞給它:

auto fut = std::async(std::launch::async, f); // 異步發射f

 

事實上, 若是有一個函數的行爲像std::async那樣,但它會自動使用std::launch::async做爲發射策略,那樣就是一個方便的工做啦!它很容易寫出來,棒極了。這是C++11的版本:

template<typename F, typename... Ts> inline std::future<typename std::result_of<F(Ts...)>::type> reallyAsync(F&& f, Ts&&... params) // 返回異步調用f(param...)的future { return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...); }

 

這個函數接收一個可調用對象f和零個或多個參數params,而且把它們完美轉發(看條款25)給std::async,傳遞std::launch::async做爲發射策略。就像std::async那樣,它返回一個類型爲f調用params的結果的std::future,決定這個結果很容易,由於std::result_of這個type trait能夠把結果給你。

reallyAsync用起來就像std::async那樣:

auto fut = reallyAsync(f); // 異步執行f,若是std::async拋異常reallyAsync也會拋異常

在C++14中,推斷reallyAsync返回值類型的能力簡化了函數聲明:

template<typename F, typename... Ts> inline auto reallyAsync(F&& f, Ts&&... params) // C++14 { return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...); }

 

這個版本很清楚地讓你知道reallyAsync除了使用std::launch::async發射策略調用std::async外,沒作任何東西。


總結

須要記住的3點:

  • std::async的默認發射策略既容許任務異步執行,又容許任務同步執行。
  • 這個靈活性(上一點)致使了使用thread_local變量時的不肯定性,它隱含着任務可能不會執行,它還影響了基於超時的wait調用的程序邏輯。
  • 若是異步執行是必需的,指定std::launch::async發射策略。

參考資料:

http://www.javashuo.com/article/p-aewdijrh-bd.html

http://www.javashuo.com/article/p-enjqqjcv-hb.html

https://blog.csdn.net/lijinqi1987/article/details/78909479

http://www.it1352.com/467359.html

相關文章
相關標籤/搜索