這裡介紹 C++ 語言的 std::async
非同步函數的使用方式,並提供一些入門的範例程式碼。
由於摩爾定律已經達到極限,現今的 CPU 都已經演變為多核心(multi-core)的架構,單一執行緒(single thread)的程式放在新的電腦中不見得可以跑得更快,若要發揮 CPU 完整的計算能力,就必須充分使用 CPU 的每一個核心。
C++11 的標準允許程式設計者透過標準的 C++ 語法,開發多執行緒且可移植的程式,讓 C++ 程式的開發更方便,其中 std::async
就是一個很實用的功能。
std::async
基本用法std::async
可以看成是 std::threads
的一個高階介面,它可以用來將比較耗時的工作分給多個執行緒平行計算,算完之後再取回結果,提高整個程式的執行效能,而其語法與 std::threads
相較之下又更單純許多,絕大部分需要平行化的 C++ 應用程式通常都會比較適合採用 std::async
。
std::async
可以使用非同步的方式呼叫普通的 C++ 函數,其第一個參數是指定要呼叫的函數,所有要傳入的參數就放在第二個參數之後,而在 std::async
執行之後,會傳回一個 std::future
的物件,最後我們再透過這個物件取回計算結果。
假設我們要用另外一個執行緒執行的函數為 work
,其函數宣告如下:
bool work (int x);
使用 std::async
呼叫 work(123)
函數的方法為:
std::future<bool> fut = std::async (wrok, 123);
呼叫 std::async
之後會立即傳回一個 std::future
的物件 ret
,隨後即可透過這個物件取得計算結果:
bool r = fut.get();
這就是 std::async
最簡單的用法。
以下是一個簡單的 std::async
範例程式 async.cpp
:
#include <future> #include <iostream> // 耗時的工作 bool is_prime (int x) { std::cout << "Calculating. Please, wait...n"; bool r = true; for (int i = 2; i < x; ++i) { if (x % i == 0) { r = false; break; } } std::cout << "We're done.n"; return r; } // sleep 函數 void sleep(int milisec) { std::this_thread::sleep_for(std::chrono::milliseconds(milisec)); } int main () { // 使用另外一個執行緒執行 is_prime(334214467) std::future<bool> fut = std::async (is_prime, 334214467); // 執行其他工作 ... sleep(200); std::cout << "Do something in main thread ...n"; sleep(200); std::cout << "Do something in main thread ...n"; sleep(200); std::cout << "Do something in main thread ...n"; // 等待並取回計算結果 bool r = fut.get(); if (r) { std::cout << "It is prime!n"; } else { std::cout << "It is not prime.n"; } return 0; }
在這段程式中,我們定義了一個 is_prime
這個檢查一個整數是否為質數的函數,而這個函數執行起來會需要比較久的時間,因此我們利用 std::async
這個非同步的方式呼叫 is_prime
,並傳入 334214467
這個參數給 is_prime
,讓它在另外一個執行緒中進行計算,而我們在主執行緒中就可以處理其他的工作,等到真正需要 is_prime
的計算結果時,再以 std::future
所提供的介面將計算的結果取回。
std::async
相關的函數定義在 <future>
這個標頭檔中,使用時記得要引入。
以 g++
編譯 std::async
的程式時,要加上 -lpthread
參數,比對於比較舊版的 g++
還要再加上 -std=c++11
:
g++ -o async -std=c++11 -lpthread async.cpp
編譯完成後,執行之:
./async
Calculating. Please, wait... Do something in main thread ... Do something in main thread ... Do something in main thread ... We're done. It is prime!
從這個輸出訊息的順序就可以看得出來,當 is_prime
函數在計算的同時,主執行緒也可以處理其他的工作,這樣就達到平行化的效果。
這裡的 334214467
是我任意挑選的一個質數,如果想測試更大或更小的質數,可從 The Prime Pages 網頁中的質數列表挑選。
std::launch
執行模式上面的範例程式若是在不同的環境下編譯,可能會產生不同的結果,也就是說拿完全一樣的程式碼,只是換一個編譯器來編譯程式,程式執行起來可能就會完全不同。
上面的範例我是在 Mac OS X 中使用 Apple LLVM 7.0.0 編出來的,若改在 CentOS Linux 7.2 中用 gcc 4.8.5 編譯,執行結果就不一樣了:
Do something in main thread ... Do something in main thread ... Do something in main thread ... Calculating. Please, wait... We're done. It is prime!
從這個輸出看起來,is_prime
函數實際被呼叫的時機點不太對,變成發生在呼叫 fut.get()
取回計算結果的那一行,也就是說程式是以同步(synchronous)的方式來執行的,沒有另外開一個平行的執行緒進行計算,這樣就跟普通的單一執行緒程式沒有差別了。
這個問題是因為 std::async
有兩種執行模式:
std::launch::async
非同步執行std::launch::deferred
延後執行std::future
物件呼叫 get()
時,才去真正執行其中的程式,而執行的方式就是一般的函數呼叫,沒有任何平行化效果。而預設值是 std::launch::async | std::launch::deferred
,也就是說編譯器可以自行決定要讓 std::async
所呼叫的函數用什麼方式執行。
std::launch::deferred
延後執行若程式設計者要自行指定執行的方式,可以在第一個參數中設定(其他的參數則往後推),例如明確設定讓函數呼叫延後執行:
// 延後執行 std::future<bool> fut = std::async (std::launch::deferred, is_prime, 334214467);
這種執行方式是屬於單一執行緒的情況。
延後執行就沒有平行的效果:
time ./async
Do something in main thread ... Do something in main thread ... Do something in main thread ... Calculating. Please, wait... We're done. It is prime! real 0m1.444s user 0m0.845s sys 0m0.000s
std::launch::async
非同步執行改為非同步執行(多執行緒平行化執行):
// 非同步執行 std::future<bool> fut = std::async (std::launch::async, is_prime, 334214467);
這種方式就會產生另外一個執行緒。
這樣執行時間就會明顯縮短:
time ./async
Calculating. Please, wait... Do something in main thread ... Do something in main thread ... Do something in main thread ... We're done. It is prime! real 0m0.840s user 0m0.840s sys 0m0.001s
以上兩張示意圖我是用 LibreOffice 畫的,需要畫類似這樣圖的人,可以下載其原始檔回去修改。
參考資料:Modernes C++、Modernes C++、BogoToBogo、Solarian Programmer、cppreference.com、oopscene’s blog、Jaka’s Corner