分類: 程式設計

C++ 使用 Async 非同步函數開發平行化計算程式教學

這裡介紹 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);

這種執行方式是屬於單一執行緒的情況。

std::launch::deferred 執行模式

延後執行就沒有平行的效果:

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);

這種方式就會產生另外一個執行緒。

std::launch::async 執行模式

這樣執行時間就會明顯縮短:

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++BogoToBogoSolarian Programmercppreference.comoopscene’s blogJaka’s Corner

G. T. Wang

個人使用 Linux 經驗長達十餘年,樂於分享各種自由軟體技術與實作文章。

Share
Published by
G. T. Wang
標籤: C/C++效能

Recent Posts

光陽 KYMCO GP 125 機車接電發動、更換電瓶記錄

本篇記錄我的光陽 KYMCO ...

2 年 ago

[開箱] YubiKey 5C NFC 實體金鑰

本篇是 YubiKey 5C ...

3 年 ago

[DIY] 自製竹火把

本篇記錄我拿竹子加上過期的苦茶...

3 年 ago