2016年7月7日木曜日

【読書ノート】Effective Modern C++ - Item 36

O'Reilly Early Releaseで購入したEffective Modern C++の読書記録です。
※この記事はあくまで私の備忘録です。(断りなく自分の解釈や考え・感想を入れたり、理由もなく内容を省略したり、間違いや曲解もあると思います。誤りの指摘は歓迎です。)

Item 36: 非同期性が重要な場合はstd::launch::asyncを指定しよう

std::launch::async => fは異なるスレッドで非同期に実行される
std::launch::deferred => fはstd::asyncで返されたfutureのgetかwaitが呼ばれた時にだけ実行される。getかwaitが呼ばれるとfは同期的に実行される。
デフォルトは両者のor、つまり以下の2つの文は同一の意味を持つ。

auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async | std::launch::deferred, f);

このデフォルト設定の場合、item 35で書かれている通り標準ライブラリに適切なスレッドの管理を任せることができる。
この場合の注意点は以下の3つ。
(auto fut = std::async(f);が実行されるスレッドをtとする。)

  • fがtと並行に(concurrentlyに)実行されるかどうか予測することはできない (fの実行がdeferredされる可能性があるため)
  • fがfutに対してgetかwaitを呼ぶスレッドと異なるスレッドで実行されるかどうか予測することはできない
  • fが実行されるかどうか予測できない場合がある(futに対してgetかwaitが呼ばれないpathがあるかもしれないため)

デフォルトのlaunch policyで注意する必要があるのはスレッドローカル変数 (thread_localで宣言する変数) を利用する場合。
なぜならfが読み込むスレッドローカルストレージ (TLS) がどのスレッドのものなのか予測できないため。

以下のコードは終了しない可能性がある。

using namespace std::literals;
void f() {
  std::this_thread::sleep_for(1s);
}
auto fut = std::async(f);
while(fut.wait_for(100ms) != std::future_status::ready) {
  …
}

fがdeferredされている場合、std::future_status::deferredが返されるため、std::future_status::readyと==にならない。
この種のバグは高負荷な状況でなければ明らかにならないため、開発やユニットテストでは容易に見逃してしまう。
それはoversubscription (ハードウェアのスレッド数を超えるソフトウェアスレッドがある状況) とスレッドの枯渇が生じた場合にfの実行の延期 (deferred) が生じるため。

この問題を回避するにはwait_forでdeferredが返ってくるかどうかを確かめれば良い。
タイムアウトを0に設定すれば即時判定が可能。

auto fut = std::async(f);
if (fut.wait_for(0s) == std::future_status::deferred) {
  … // futにwaitかgetを使用してfを同期的に呼び出す
}
else {
  while(fut.wait_for(100ms) != std::future_status::ready) {
    … // fは並行処理されているのでそれが完了するのを待つ
  }
  … // この時点でfutはreadyに
}

結論として、デフォルトのlaunch policyを使用して良いのは以下の条件が満たされる場合である。

  • タスクはgetまたはwaitを呼ぶスレッドと並行に実行される必要は無い
  • どのスレッドのthread_local変数が読み書きされても問題にならない
  • std::asyncが返すfutureに対してgetまたはwaitが呼ばれる保証があるか、もしくはタスクがずっと実行されない場合でも問題ない
  • wait_forまたはwait_untilを使用するコードではdeferred statusになっている可能性を考慮している

上記の条件のうち1つでも満たされない場合はstd::lanuch::asyncを指定する。

auto fut = std::async(std::launch::async, f);

std::launch::asyncを自動的に付加するために以下のようなユーザー定義関数を用意するのも良い手。

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