std::futureを使う側が先に死ぬ場合の処理

C++11での話。もしかしたら、もっと良い方法があるかもしれないけどstd::futureの使い方について自分用にメモを残しておく。

前提

  • マルチスレッド動作
  • 片方のスレッドでひたすら仕事を処理する(ワーカースレッド的な)
  • 仕事の内容を選ばないようにstd::functionのリストで管理する(今回は本筋じゃないのでコード上は省略している)
  • もう片方はスレッドに仕事を依頼する
  • 仕事を依頼した後、futureで仕事の結果をもらう
  • 時間がかかりすぎるときには、futureの結果を待たずに終了する

ダメな書き方

#include <future>
#include <chrono>
#include <thread>
#include <functional>
#include <iostream>

using namespace std;

class Test
{
public:
	void waitOtherThread(void);
	~Test();
private:
	void doOtherThread(promise<int> &p);
	thread th;
};

void Test::waitOtherThread(void)
{
	promise<int> p;	// ここをローカル変数にしているのでこのメソッドから抜けると廃棄される
	future<int> f = p.get_future();

	// void()のリストでスレッドにわたす前提なのでbindを使う
	// std::functionは引数のコピーを作るのでcopyableじゃないとわたせない。
	// 代わりに参照を渡すことができる
	// [2017/09/19 追記]
	// 上記のコメントは間違っているので訂正。
	// std::bind()は引数のコピーを作るので参照を受け取るdoOtherThread()のI/Fと一致しない。
	// そのせいで?コンパイルエラーになる。(推測)
	// I/Fを一致させ、かつコピー可能とするために
	// std::ref()を使用してstd::reference_wrapperを渡す。
	// [2017/09/19 追記]
	function<void()> func = bind(&Test::doOtherThread, this, ref(p));
	th = thread(func);

	// 20ms以上時間がかかる場合はタイムアウト
	future_status status = f.wait_for(chrono::milliseconds(20));
	if (status != future_status::timeout) {
		f.get();
	}
}
Test::~Test()
{
	if (th.joinable()) {
		th.join();
	}
}
void Test::doOtherThread(promise<int> &p)
{
	// 今回わざとタイムアウトさせるためにウェイトを入れる
	this_thread::sleep_for(chrono::milliseconds(50));

	// 元がローカル変数なので、先に依頼元が終了していると無効なアクセスになる
	p.set_value(1);
}

int main(void)
{
	Test t;

	t.waitOtherThread();
}

これを実行すると以下のように例外が発生する。

terminate called after throwing an instance of 'std::future_error'
what(): std::future_error: Promise already satisfied
Aborted (core dumped)

回避したコード

以下2点が問題の原因だった。

  • ローカル変数を渡している点
  • ローカル変数を握っているメソッドが先に終了する点

タイムアウトで先にメソッドが終了するのは今回の要求事項なので、だったらローカル変数よりライフタイムの長いものを渡せば良いって話で。とりあえずshared_ptrにしてみた。

#include <future>
#include <chrono>
#include <thread>
#include <functional>
#include <iostream>
#include <memory>

using namespace std;

class Test
{
public:
	void waitOtherThread(void);
	~Test();
private:
//	void doOtherThread(shared_ptr<promise<int>> &p);	// 2017/09/06 間違い
	void doOtherThread(shared_ptr<promise<int>> p);
	thread th;
};

void Test::waitOtherThread(void)
{
	shared_ptr<promise<int>> p = make_shared<promise<int>>();
	future<int> f = p->get_future();

	// shared_ptrの指すpromiseの実体をスレッド側へ持って行くためにstd::move()する
	function<void()> func = bind(&Test::doOtherThread, this, move(p));
	th = thread(func);

	future_status status = f.wait_for(chrono::milliseconds(20));
	if (status != future_status::timeout) {
		f.get();
	}
}
Test::~Test()
{
	if (th.joinable()) {
		th.join();
	}
}
//void Test::doOtherThread(shared_ptr<promise<int>> &p)		// 2017/09/06 間違い
void Test::doOtherThread(shared_ptr<promise<int>> p)
{
	this_thread::sleep_for(chrono::milliseconds(50));

	p->set_value(1);
}

int main(void)
{
	Test t;

	t.waitOtherThread();
}

これなら例外発生せず実行できた。

2017/09/06 00:35
うっかり「&」をつけたままだった。ソースコードを修正した。

C++14の場合

ラムダ式でムーブキャプチャできるらしいので、ラムダ式の内部へムーブコンストラクタでpromiseを持って行くことができる。

#include <future>
#include <chrono>
#include <thread>
#include <functional>
#include <iostream>
#include <memory>

using namespace std;

class Test
{
public:
	void waitOtherThread(void);
	~Test();
private:
	thread th;
};

void Test::waitOtherThread(void)
{
	promise<int> p;		// ローカル変数で大丈夫
	future<int> f = p.get_future();

	// ムーブキャプチャでラムダ式内部へpromiseを持って行く
	// ラムダ式はconst付きメソッド扱いとのこと
	// set_value()はconstではないのでmutable指定をつける
	th = thread( [ inner_p{move(p)} ]() mutable {
		this_thread::sleep_for(chrono::milliseconds(50));
		inner_p.set_value(1);
	} );

	future_status status = f.wait_for(chrono::milliseconds(20));
	if (status != future_status::timeout) {
		f.get();
	}
}
Test::~Test()
{
	if (th.joinable()) {
		th.join();
	}
}

int main(void)
{
	Test t;

	t.waitOtherThread();
}

実行すると例外発生せず無事?終了する。