2016年8月25日木曜日

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

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

Item 37: 全てのパスでstd::threadがunjoinableとなるようにすること

joinableなstd::threadのデストラクタが呼ばれると強制終了される

std::threadインスタンスは"joinable"と"unjoinable"の2つのいずれかの状態にある。
joinableは、スレッドが実行されているかもしくは実行可能な状態。たとえばブロックされていたり、待っていたり、実行完了したスレッドが該当する。
unjoinableは、joinableでないスレッドのことで、以下の場合を含む。

  • デフォルトコンストラクタで初期化されたstd::thread
  • moveされた後のstd::thread
  • 既にjoinされたstd::thread
  • detachされたstd::thread

std::threadがjoinableであるかどうかが重要な理由の1つは、joinableなstd::threadのデストラクタが呼ばれるとプログラムがterminateされること。
たとえば以下のプログラムではif文に入らないか例外が発生した時にt.join()が呼ばれずプログラムがterminateされる。

constexpr auto tenMillion = 10000000;

bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion)
{
  std::vector<int> goodVals;
  std::thread t([&filter, maxVal, &goodVals] {
    for (auto i = 0; i <= maxVal; ++i) {
      if (filter(i)) goodVals.push_back(i);
    }
  });
  auto nh = t.native_handle();
  … // tの優先度を設定
  if (conditionsAreSatisfied()) {
    t.join();
    performComputation(goodVals);
    return true;
  }
  return false;
} 

なお、通常はitem 35で学習したtask-based設計 (std::asyncを使うやり方) を採用した方が良いがここではスレッドの優先度を指定するためstd::threadを利用してnative_handleを呼び出している。
ただし優先度は本来std::threadの実行前、サスペンド状態のまま設定した方が良いが、これについてはitem 39で扱う。
ちなみにC++14では以下のように数値を表記できる。

constexpr auto tenMillion = 10'000'000;

話を戻し、joinableなstd::threadのデストラクタが呼ばれると、なぜプログラムがterminateされてしまうようになっているのか?
それを考えるにはそうしない2つのケースについて考えると良い。

暗黙的にjoinする場合

これはstd::threadのデストラクタがスレッドの実行が完了するのを待つことを意味する。
そうなるとやっかいなパフォーマンス問題が生じる可能性がある。
上記のプログラムの場合、conditionAreSatisfied()がfalseを返しているにも関わらずフィルタ処理を行っていることとなる。
これは明らかにプログラマの意図に反する。

暗黙的にdetachする場合

この場合、std::threadインスタンスとスレッド実行が切り離され、スレッドはバックグラウンドで実行され続ける。
これは一見暗黙的なjoinよりかはマシに見えるが、デバッグ上の問題でより悪くなる可能性がある。
たとえばスレッドに渡しているラムダ式ではgoodValsを参照キャプチャしているが、これはローカル変数なのでdoWorkを抜けると使用できなくなる。
具体的には、doWorkの呼び出し元が別の関数を呼び出し、doWork内のgoodValsのメモリ領域を再利用するとgoodValはもう使えなくなる。
この問題が生じた場合のデバッグの困難さは容易に想像できる。

以上の理由により、標準化委員会はjoinableなstd::threadを破棄することを禁止した。
つまり、プログラムをterminateさせるようにした。

RAIIなthreadクラスによる強制終了の回避

しかし実際のところ全てのpathでunjoinableになることを確認することは重荷である。
return, continue, goto, それに例外などスコープを抜けてしまう要因は多い。
それに対する通常の対処法はローカルオブジェクトのデストラクタを利用すること、つまりRAII (Resource Acquisition Is Initialization) オブジェクトを利用することである。
表記に使われているのはinitializationではあるが重要なのはdestruction。
例としてはSTLコンテナ、スマートポインタ、std::fstreamなど。
しかし標準化委員会としてはstd::threadをデストラクタでどうすべきなのかは分からないのでjoinもdetachもデストラクタとして採用されていない。
ただし自分でstd::threadをRAIIクラスに書くことは容易である。

class ThreadRAII {
public:
  enum class DtorAction {join, detach};

ThreadRAII(std::thread&& t, DtorAction a)
  : action(a), t(std::move(t)) {}

~ThreadRAII() {
  if (t.joinable()) {
    if (action == DtorAction::join) {
      t.join();
    } else {
      t.detach();
    }
  }
}

std::thread& get() {return t;}

private:
  DtorAction action;
  std::thread t;
};

コンストラクタでrvalueを取っているのはstd::threadがnoncopyableであるため。

std::threadは初期化される時に実行が始まるため、他のデータメンバの初期化を先に済ませるため、std::threadはデータメンバの中で最後に宣言すると良い。

get関数を用意し必要に応じてstd::threadを取り出せるようにしている。
それによってstd::threadのインターフェースを全て実装せずに済んでいる。

unjoinableなstd::threadに対してjoinまたはdetachをすると未定義動作となるので、事前にt.joinable()でチェックしている。
たとえばgetによって取得されたstd::threadがmoveやjoinやdetachが呼ばれている場合はunjoinableになっている。
また、別のスレッドがメンバ関数を呼ぶことでデストラクタと同時に処理が進む場合、t.joinable()を読んだ時はjoinableで、その後のt.join() / t.detach()を呼ぶ時にunjoinableになるという競合があり得る。
しかし一般的には1つのオブジェクトに対してスレッドセーフに呼び出せるのはconstメンバ関数に限られる (item 16参照) ので、これを守っている限りは生じない競合である。

このThreadRAIIクラスを利用して冒頭のプログラムを記述すると以下のようになる。

bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion)
{
  std::vector<int> goodVals;
  ThreadRAII t(std::thread([&filter, maxVal, &goodVals] {
      for (auto i = 0; i <= maxVal; ++i) {
        if (filter(i)) goodVals.push_back(i);
      }
    }), ThreadRAII::DtorAction::join);
  auto nh = t.get().native_handle();
  …
  if (conditionsAreSatisfied()) {
    t.get().join();
    performComputation(goodVals);
    return true;
  }
  return false;
}

ここではパフォーマンス上のやっかいな問題を受容してThreadRAIIのデストラクタオプションでjoinを選択している。
未定義動作とプログラムの強制終了に比べるとマシだという判断である。
ただしItem 39で示すがjoinするやり方はプログラムがhungする(フリーズする)ケースも存在し得る。
それに対する適切な解法はもう処理を続けるひつようの無いlambda式に対してすぐに終了するよう伝達することであるが、このようなinterruptible threadはstd::threadはC++11ではサポートしておらず、自分で書く必要があるがそれは本書のスコープ外とする。
(Anthony Williamsの"C++ Concurrency in Action" (Manning Publications, 2012)のsection 9.2.に良いやり方が載っている。)

Item 17で説明したように、ThreadRAIIではデストラクタが宣言されているためmove操作はコンパイラで自動生成されない。
ThreadRAIIがmoveされてはならない理由はないので、自動生成をリクエストする場合は以下の記述を加えれば良い。

class ThreadRAII {
  …
  ThreadRAII(ThreadRAII&&) = default; // support
  ThreadRAII& operator=(ThreadRAII&&) = default; // moving
  … 
};