yohhoyの日記

技術的メモをしていきたい日記

C++11 FuturesとBoost.Thread Futuresの比較

C++11標準ライブラリで新たに追加された Futuresライブラリ/標準ヘッダfuture と、Boost.Threadライブラリ(Boost 1.48.0) の機能比較。

両者のFutures機能*1が提供するクラスと関数の対応関係を下表にまとめる*2C++0xドラフト段階からBoost.Threadライブラリをベースとしているため、両ライブラリの基本的な設計方針は一致している。

2012-11-08追記:Boost.Thread 1.50.0からboost::unique_futureまたはboost::futureを切り替え可能となっている(旧unique_futureは廃止予定とされており1.55.0までの提供)。また、Boost.Thread 1.52.0からboost::async関数(launch::asyncポリシーのみ対応)が追加された。

C++11 Futures Boost.Thread Futures
std::futureクラス
(→id:yohhoy:20120131)
boost::unique_futureクラス[1.55.0まで選択可]
boost::futureクラス[1.50.0以降で選択可]
std::shared_futureクラス
(→id:yohhoy:20120201)
boost::shared_futureクラス
std::promiseクラス
(→id:yohhoy:20120131)
boost::promiseクラス
std::packaged_taskクラス
(→id:yohhoy:20120202)
boost::packaged_taskクラス
std::async関数
(→id:yohhoy:20120203)
boost::async関数[1.52.0以降]
(対応無し) boost::wait_for_any関数
boost::wait_for_all関数

std::future/shared_future vs. boost::unique_future/shared_future

  • 状態確認(is_ready, has_value, has_exception, get_statusメンバ関数)はBoost.Threadにのみ存在する。C++11では直接getメンバ関数で値/例外を取り出すか、タイムアウト付き待機関数で状態を確認できる。*3
  • タイムアウト付き待機関数のメンバ関数名が異なる。C++11ではwait_for, wait_untilに対し、Boost.Threadではtimed_wait, timed_wait_untilとなる。
  • タイムアウト付き待機関数の戻り値型が異なる。C++11ではfutureの状態(std::future_status列挙型)を返すが、Boost.Threadではbool型を返す。*4
    • Boost.Threadでもboost::future_status列挙型に変更された。[1.50.0以降]
  • validメンバ関数C++11にのみ存在する。
    • Boost.Threadにもvalidメンバ関数が追加された。[1.50.0以降]

std::promise vs. boost::promise

  • set_XXX_at_thread_exit系メンバ関数C++11にのみ存在する。C++11で新しく導入されたthread_local変数に対応するための機能であり、C++11にしか存在しないのは当然。
  • set_wait_callbackメンバ関数はBoost.Threadにのみ存在する。C++11ではstd::async関数+std::launch::deferredポリシー動作が近い機能を実現する。(具体例を後述)

std::packaged_task vs. boost::packaged_task

  • テンプレート引数に指定する型の要件が異なる。C++11では関数型を指定するのに対し、Boost.Threadは戻り値型を指定する。
  • C++11(std::packaged_task)では任意の関数オブジェクトを指定できる。コンストラクタで引数バインドを行うか、operator()で引数を渡すかを選択可能。
  • Boost.Thread(boost::packaged_task)では引数無し関数オブジェクトのみ指定できる。引数無しのoperator()のみが提供されており、引数をとる関数オブジェクトを指定する場合は予めboost::bindでバインド処理が必要。
  • make_ready_at_thread_exitメンバ関数C++11にのみ存在する。
  • resetメンバ関数C++11にのみ存在する。
    • Boost.Threadにもresetメンバ関数が追加された。[1.50.0以降]
  • set_wait_callbackメンバ関数はBoost.Threadにのみ存在する。C++11ではstd::async関数+std::launch::deferredポリシー動作が近い機能を実現する。(具体例を後述)
int func(int, int);  // 対象の関数
int arg1, arg2;      // 関数funcに渡す引数

//-------- C++11
#include <future>
auto task1 = std::packaged_task<int()>(func, arg1, arg2);
task1();
auto task2 = std::packaged_task<int(int, int)>(func);
task2(arg1, arg2);

//-------- Boost.Thread
#include <boost/thread/packaged_task.hpp>
auto task1 = boot::packaged_task<int>(std::bind(func, arg1, arg2));
task1();

std::async関数C++11のみ)

Boost.Threadにもlaunch::asyncポリシー+boost::async関数が追加された(1.52.0時点ではlaunch::deferredポリシー未実装)。[1.52.0以降]
std::launch::asyncポリシーを指定したstd::async関数の動作は、boost::threadとboost::packaged_taskクラスを用いたエミュレーションが可能。*5[1.51.0まで]

int func(int, int);  // 対象の関数
int arg1, arg2;      // 関数funcに渡す引数

//-------- C++11 async(launch::async)
#include <future>
auto f = std::async(launch::async, func, arg1, arg2);
// 別スレッドで関数funcが実行される
int result = f.get();

//-------- Boost.Threadによるエミュレーション
#include <boost/thread.hpp>
boost::packaged_task<int> task(boost::bind(func, arg1, arg2));
boost::unique_future<int> f = task.get_future();
boost::thread th(boost::move(task));
int result = f.get();

std::launch::deferredポリシーを指定したstd::async関数の動作は、boost::packaged_task::set_wait_callbackメンバ関数を用いてエミュレーション可能。*6

int func(int, int);  // 対象の関数
int arg1, arg2;      // 関数funcに渡す引数

//-------- C++11 async(launch::deferred)
#include <future>
auto f = std::async(launch::deferred, func, arg1, arg2);
int result = f.get();  // f.get()呼出時に関数funcが実行される

//-------- Boost.Threadによるエミュレーション
#include <boost/thread.hpp>
boost::packaged_task<int> task(boost::bind(func, arg1, arg2));
task.set_wait_callback([](boost::packaged_task<int>& pt) {
  try { pt() } catch (boost::task_already_started&) {}
});
boost::unique_future<int> f = task.get_future();
int result = f.get();

boost::wait_for_all関数(Boost.Threadのみ)

指定された全futureオブジェクトの非同期処理完了を待機する関数。C++11標準ライブラリでは同関数が提供されないが、単に全futureオブジェクトのwaitメンバ関数を呼び出せば同じ効果が得られる。

// wait_for_all(f1, f2)相当
f1.wait(); f2.wait();

boost::wait_for_any関数(Boost.Threadのみ)

指定されたfutureオブジェクト群のうち、少なくともどれか1つの非同期処理が完了するまで待機する関数。C++11標準ライブラリでは同関数は提供されないため、同じ機能を実現するにはstd::condition_variableを用いて完了通知機構を組み立てる必要がある。

下記コードは2つの別スレッド上の非同期処理のうち、少なくとも1つが処理完了するまで待機する実装例。

#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>

struct waiter_t {
  std::mutex m;
  std::condition_variable cv;
  bool done[2];
  waiter_t() : done{false,false} {}
} waiter;

void call_and_notify(std::packaged_task<int()> task, int id)
{
  task();
  std::lock_guard<std::mutex> lk(waiter.m);
  waiter.done[id] = true;  // 自タスクの完了フラグを設定
  waiter.cv.notify_all();  // 条件変数waiter.cvで待機する全スレッドに通知
}

// 非同期タスクの定義とfuture取り出し
auto task1 = std::packaged_task<int()>([&]()->int{ /* 処理1 */ });
auto task2 = std::packaged_task<int()>([&]()->int{ /* 処理2 */ });
auto f1 = task1.get_future(), f2 = task2.get_future();
// call_and_notify経由で非同期タスクを実行
std::thread th1(call_and_notify, std::move(task1), 0);
std::thread th2(call_and_notify, std::move(task2), 1);

std::unique_lock<std::mutex> lk(waiter.m);
// 条件(done[0] || done[1])が偽の間は条件変数waiter.cvで待機
waiter.cv.wait(lk, [&]{
  return (waiter.done[0] || waiter.done[1]);
});
// done[0], done[1]の少なくとも一方はtureになっている
int first_result = waiter.done[0] ? f1.get() : f2.get();  // get呼出はブロックしない

参考URL

*1:"Futures"に対応する適切な日本語訳がみあたらないため、そのまま英語表記としている。future/promiseについてはwikipedia:futurewikipedia:en:futures_and_promisesも参照のこと。

*2:正確にはクラステンプレートと関数テンプレートとして提供される。本記事では簡単のためテンプレートであることをいちいち明記しない。

*3:C++11標準ライブラリからはN2996で削除された。

*4:N3058タイムアウト付き待機関数の戻り値型が変更された。

*5:http://www.boost.org/doc/html/thread/synchronization.html#thread.synchronization.futures.creating

*6:http://www.boost.org/doc/html/thread/synchronization.html#thread.synchronization.futures.lazy_futures