C++で簡単非同期処理(std::thread,std::async)

  • 177
    Like
  • 2
    Comment
More than 1 year has passed since last update.

C++11では<future>ヘッダが導入され、
簡単に非同期の処理が実装できるようになった。
以下では用途毎に実装方法をまとめる。
thread間での同期の話はせず、あくまで完全に非同期な処理についてまとめる。

戻り値が必要ない場合(Thread-per-message pattern)

とりあえず非同期に実行できればいい場合。
実行時間を短縮するために複数の独立な処理を並列に実行するなどの応用が考えられる。
std::threadを使用する。最も基本的な使い方は以下の通りである:

auto th1 = std::thread([]{ do_long_work(); });
do_another_things();
th1.join();

std::threadの引数に実行したい関数を渡す。
関数に引数を与えたい場合は、std::thread(func, arg)のように行う。
プログラムが終了するまでにはjoin()しないといけない。
std::threadの引数のlambda式はキャプチャする事もできる:

double a;
auto th1 = std::thread([&a]{ a = long_calc(); });
do_another_things();
th1.join();
std::cout << a << std::endl; // 1.0

std::vector<std::thread>のようにコンテナに入れる事もできる。

#include <vector>
#include <future>
#include <iostream>

int main(int argc, char const *argv[]) {
  std::vector<int> v(10);
  std::vector<std::thread> threads;
  for (int i = 0; i < 10; ++i) {
    threads.push_back(std::thread([i, &v] { v[i] = i * i; }));
  }
  for (std::thread &th : threads) {
    th.join();
  }
  for (int i : v) {
    std::cout << i << std::endl;
  }
  return 0;
}

参考文献
- std::thread - cppreference
- std::threadをあとから開始。それとムーブ対応したコンテナについて (vectorに入れる部分)

コンパイル時の注意

コンパイルする際は並列用のライブラリをリンクする事を忘れずに。
例えばLinuxではpthreadをリンクする。

$ g++ -std=c++11 source.cpp -pthread
$ clang++ -std=c++11 source.cpp -pthread

リンク忘れると実行時にエラーになる:

terminate called after throwing an instance of 'std::system_error'
  what():  Enable multithreading to use std::thread: Operation not permitted

結果を取得したい場合

上述の方法でも戻り値を参照でキャプチャした変数に代入する事ができているが、
これでは不便な場合も多い。
そこで登場するのがstd::asyncである。

auto result = std::async(std::launch::async, [] { return long_calc(); });
do_another_things();
std::cout << result.get() << std::endl;

resultの型はstd::future<T>(Tlong_calc()の戻り値の型)である。
result.get()ではじめて作成したスレッドをjoin()するので、
do_another_things()はthreadの生成直後に(long_calc()の実行を待たずに)実行される。

非同期処理をどうやって実行するか(Policy)

第一引数のstd::launch::asyncはpolicyを指定しており、
別スレッドを開始してlong_calc()を計算する、という指定である。
Policyは次の4通りの指定ができる

  • std::launch::async : 別スレッドで実行
  • std::launch::deferred : 遅延評価
  • std::launch::async | std::launch::deferred : 上記のいずれか(実装依存)
  • 指定なし : 両方指定した場合と同様

std::launch::deferredを指定した場合は単に遅延評価になる。
つまり最初にresult.get()が呼ばれたタイミングでlong_calc()を評価する。
次回以降にresult.get()が呼ばれたら最初の時に計算した値を返す。

std::async(std::launch::async | std::launch::deferred, func, arg)のように両方指定する場合、
あるいは単にstd::async(func, arg)のように省略した場合は、
async/deferredのどちらになるか実装依存である。

vectorを初期化する場合は以下のようにする:

#include <future>
#include <iostream>
#include <vector>

int main(int argc, char const *argv[]) {
  std::vector<std::future<int> > v;
  for (int i = 0; i < 10; ++i) {
    v.push_back(std::async([i] { return i * i; }));
  }
  for (auto &val : v) {
    std::cout << val.get() << std::endl;
  }
}

参考文献
- std::async - cppreference
- std::future - cppreference
- C++/CXの旅(9):C++11の標準タスク処理のまとめ(std::future/promise/thread/async)
- GNU/LinuxのC++11でプログラミングの常識がひっくりかえった(非同期の例外について詳しい)

Thread Pool (Worker Thread) pattern

C++でスレッドプール(ワーカースレッド)パターンを実装する方法は方々で議論されている。

参考文献

次回に詳しくまとめる。