yohhoyの日記(別館)

もうちょい長めの技術的メモをしていきたい日記

C++スレッド遅延開始の実装5パターン

局所的に「スレッド開始を遅延させる」ネタが盛り上がっていたので、C++とBoostライブラリを用いた色々な実装方法をまとめてみました。

この記事で対象とするのは、下記コードにある2つの要件を満たす実装方法です。

  • (1) スレッドを管理するオブジェクトXのコンストラクト時ではなく、その後の任意タイミングで新スレッド処理を開始する。
  • (2) オブジェクトXのデストラクト時に、上記(1)の別スレッドがまだ実行中ならそのスレッド処理完了を待機する。
class X {
  // threadオブジェクトを保持するメンバ変数

  void do_() { /* 別スレッド処理 */ }

public:
  ~X()
  {
    // (2) 別スレッドがまだ実行中なら完了を待機する
  }

  void start()
  {
    // (1) 新しいスレッドを開始して関数do_を実行する
  }
};

int main()
{
  X x;
  x.start();
}

実装方法の比較

#1,2,3は@cpp_akiraさんの std::threadをあとから開始。それとムーブ対応したコンテナについて - Faith and Brave - C++で遊ぼう、#4は@egtraさんのイグトランスの頭の中(のかけら) » Boost.ThreadとBoost.Optionalからのパク(ry引用です。#5が新たに追加した実装方法です。

実装方法 C++03/Boost C++11/Boost C++11/std
#1 SmartPtr+thread
#2 thread.swap
#3 threadムーブ
#4 Boost.Optimal+thread ×
#5 future+async() × ×

表中の"C++03/Boost"はC++03準拠コンパイラ+Boostライブラリ、"C++11/Boost"はC++11準拠コンパイラ+Boostライブラリ、"C++11/std"はC++11準拠コンパイラ+標準ライブラリのみの範囲において、各実装方法が実現可能/不可を示します。(なお、Boost 1.49.0を前提としています。)

注意:後述コードサンプルではC++11ラムダ式を使用しており、C++11非準拠コンパイラではコンパイルできません。C++03の範囲内では下記の書き換えが必要です。

boost::thread([this] { do_(); })
// ↓
boost::thread(&X::do_, this)

#1: SmartPtr+thread

#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>

class X {
  boost::shared_ptr<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_.reset(new boost::thread([this] { do_(); }));
  }
};

(個人的には、コピー可能なスマートポインタshared_ptrより、ムーブのみを許容するunique_ptrの方が好ましい気がします。)

#2: thread.swap

#include <boost/thread.hpp>

class X {
  boost::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    boost::thread thd([this] { do_(); });
    thread_.swap(thd);
  }
};

#3: threadムーブ

#include <thread>

class X {
  std::thread thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_.joinable())
      thread_.join();
  }

  void start()
  {
    thread_ = std::thread([this] { do_(); });
  }
};

#4: Boost.Optimal+thread

#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>
#include <boost/thread.hpp>

class X {
  boost::optional<boost::thread> thread_;

  void do_() { /*...*/ }

public:
  ~X()
  {
    if (thread_)
      thread_->join();
  }

  void start()
  {
    thread_ = boost::in_place([this] { do_(); });
  }
};

#5: future+async()

この実装方法は前述のものとは異なり、threadオブジェクトの明示的な生成/保持を行いません。代わりに、launch::asyncポリシーasync関数によるスレッド開始と、futureオブジェクトのデストラクタによる(暗黙的)スレッド完了待機を行います。暗黙的なスレッドjoinについてはasync関数launch::asyncポリシーとfutureのちょっと特殊な動作 - yohhoyの日記を参照下さい。

#include <future>

class X {
  std::future<void> task_;

  void do_() { /*...*/ }

public:
  // task_がlaunch::async起動されたfutureオブジェクトの場合に限り、
  // future<void>::~future()にてスレッドjoinが暗黙的に行われる。

  void start()
  {
    task_ = std::async(std::launch::async, [this] { do_(); });
  }
};

実装#1~#4では関数do_()から例外送出されるとプログラム終了(std::terminate)しますが、実装#5は関数do_()で送出した例外を無かったこととして処理続行します。正確に表現すると、例外はちゃんとfutureオブジェクト中に格納されています。上記コードではfuture::get()による処理結果の取り出しを行っておらず、結果として関数do_()が送出した例外が失われています。

各実装はgcc 4.6.3@Ubuntu 11.10にて動作確認済みです。(なお#5はgcc 4.7.0@MacOS X 10.6だとdo_からの例外送出時のみSegmentation Faultが発生。)