スレッド内で投げられた例外をつかまえる
ここまで、さまざまなスレッドの起動と結果の引取りを紹介しましたが、忘れちゃならないのが例外処理。例外処理は、例外がthrowされた地点から呼び出し履歴をさかのぼってcatch節に辿り着くことで実現されています。
例外をthrowする(かもしれない)関数:void sigma_reference(int n, int& result) をスレッド内で動かして結果を求めることを考えてみます。
#include <thread>
#include <iostream>
#include <functional> // ref
#include <stdexcept> // invalid_argument
#include <string> // to_string
/*
* 0+1+...+n を求め resultにセットする
* n < 0 のときは invalid_argument 例外をthrowする
*/
void sigma_reference(int n, int& result) {
if ( n < 0 ) {
throw std::invalid_argument(std::to_string(n) + " は勘弁して");
}
int sum = 0;
for ( int i = 0; i <= n; ++i ) {
sum += i;
}
result = sum;
}
/*
* ダメな例: スレッド内で起きた例外はcatchできない
*/
int main() {
using namespace std;
int result;
for ( int n : { 5, -3, 10 } ) try {
thread thr(sigma_reference, n, ref(result));
thr.join();
cout << "1+2+...+" << n << "= " << result << endl;
} catch ( invalid_argument& err ) {
// main と sigma_reference は 呼び/呼ばれの関係にないため
// このcatch節には到達できない
cerr << err.what() << endl;
}
}

……ダメですねぇ。sigma_reference()内でthrowされたinvalid_argument例外がスレッド起動側のcatch節に飛び込んではくれないんです。main()はスレッドを起こし、その中でsigma_reference()を動かしていますが、両者は別のコンテキストで動いてて両者に呼び/呼ばれの関係がないので呼び出し履歴をさかのぼってもmain()のcatch節には到達できないんです。
この問題を解決すべく、例外の受け皿として機能するstd::exception_ptrが用意されています。使い方は以下のとおり:
まずスレッドの着火点となる関数オブジェクトの入り口であらゆる例外をいったんcatch(...)でつかまえます。関数:std::current_exception()はその時点でthrowされている例外が詰め込まれたexception_ptrを返すので、これをスレッドの起動元に戻します。起動元では引き取ったexception_ptrを引数にstd::rethrow_exception()を呼ぶことでスレッド内で起こった例外を再throwできます。
#include <iostream>
#include <thread>
#include <functional> // ref
#include <stdexcept> // invalid_argument
#include <exception> // current_exception/rethrow_exception
#include <string> // to_string
/*
* 0+1+...+n を求め resultにセットする
* n < 0 のときは invalid_argument 例外をthrowする
*/
void sigma_reference(int n, int& result) {
if ( n < 0 ) {
throw std::invalid_argument(std::to_string(n) + " は勘弁して");
}
int sum = 0;
for ( int i = 0; i <= n; ++i ) {
sum += i;
}
result = sum;
}
/*
* sigma_referenceをwrapする。
*/
void sigma_reference_wrap(int n, int& result, std::exception_ptr& exp) try {
sigma_reference(n, result);
} catch (...) {
// catchされた例外(がwrapされたexception_ptr)をセットする
exp = std::current_exception();
// スレッドはこの時点で終了する
}
int main() {
using namespace std;
int result;
for ( int n : { 5, -3, 10 } ) try {
exception_ptr exp;
thread thr(sigma_reference_wrap, n, ref(result), ref(exp));
thr.join();
// スレッド内で例外が発生していたなら再throw
if ( exp ) {
rethrow_exception(exp);
}
cout << "1+2+...+" << n << "= " << result << endl;
} catch ( invalid_argument& err ) { // 再throwされた例外をcatch
cerr << err.what() << endl;
}
}

std::promiseは結果だけでなく例外の受け皿も兼ねているので、少しばかりエレガントな実装となります。
#include <iostream>
#include <thread>
#include <functional> // ref
#include <stdexcept> // invalid_argument
#include <exception> // current_exception/rethrow_exception
#include <string> // to_string
#include <future> // future/promise
/*
* 0+1+...+n を求め resultにセットする
* n < 0 のときは invalid_argument 例外をthrowする
*/
void sigma_promise(int n, std::promise<int> result) try {
if ( n < 0 ) {
throw std::invalid_argument(std::to_string(n) + " は勘弁して");
}
int sum = 0;
for ( int i = 0; i <= n; ++i ) {
sum += i;
}
result.set_value(sum);
} catch ( ... ) { // 全例外を捕まえて promiseにセットする
result.set_exception(std::current_exception());
}
int main() {
using namespace std;
for ( int n : { 5, -3, 10 } ) try {
promise<int> prm;
future<int> result = prm.get_future();
thread thr(sigma_promise, n, move(prm));
thr.detach();
// 例外のthrowはfuture<T>::get()で起こるので、
// この位置からtryしてもいい
cout << "1+2+...+" << n << "= " << result.get() << endl;
} catch ( invalid_argument& err ) { // 再throwされた例外をcatch
cerr << err.what() << endl;
}
}
packaged_task、async()を使うとこんなカンジ、あたかもスレッド内から飛んできた例外を直接捕まえてるように見えますね。
#include <iostream>
#include <thread>
#include <utility> // move
#include <stdexcept> // invalid_argument
#include <string> // to_string
#include <future> // future/pakcaged_task
/*
* 0+1+...+n を返す
* n < 0 のときは invalid_argument 例外をthrowする
*/
int sigma(int n) {
if ( n < 0 ) {
throw std::invalid_argument(std::to_string(n) + " は勘弁して");
}
int sum = 0;
for ( int i = 0; i <= n; ++i ) {
sum += i;
}
return sum;
}
int main() {
using namespace std;
for ( int n : { 5, -3, 10 } ) try {
packaged_task<int(int)> pkg(sigma);
future<int> result = pkg.get_future();
thread thr(move(pkg), n);
thr.detach();
cout << "1+2+...+" << n << "= " << result.get() << endl;
} catch ( invalid_argument& err ) {
// 再throwされた例外をcatch
// スレッド内から直接throwされたかのように見えるのがナイス!
cerr << err.what() << endl;
}
}
/* async() 使うとこうなる↓*/
int main() {
using namespace std;
for ( int n : { 5, -3, 10 } ) try {
future<int> result = async(sigma, n);
cout << "1+2+...+" << n << "= " << result.get() << endl;
} catch ( invalid_argument& err ) { // 再throwされた例外をcatch
cerr << err.what() << endl;
}
}
標準C++スレッドサポートライブラリよくできてますよねー。僕のおシゴトはWindows/Linuxのどちらでも使えるツール/ライブラリを見つけてきたり、評価/調査したり、なければ作ったりportしたり……が少なくないのですが、標準ライブラリのおかげで一本書けば両者に対応できてとっても重宝しています。
C++14でstd::shared_mutexがスレッドサポートライブラリに仲間入りしています。コレについてはまたいずれ、回を改めて紹介したいと思ってます。
