CodeZine(コードジン)

特集ページ一覧

C++11:スレッド・ライブラリひとめぐり【補足編:1】

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2018/01/10 14:00
目次

スレッド内で投げられた例外をつかまえる

 ここまで、さまざまなスレッドの起動と結果の引取りを紹介しましたが、忘れちゃならないのが例外処理。例外処理は、例外がthrowされた地点から呼び出し履歴をさかのぼってcatch節に辿り着くことで実現されています。

 例外をthrowする(かもしれない)関数:void sigma_reference(int n, int& result) をスレッド内で動かして結果を求めることを考えてみます。

list-07 例外をつかまえそこねる
#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できます。

list-08 exception_ptrで例外を引き渡す
#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は結果だけでなく例外の受け皿も兼ねているので、少しばかりエレガントな実装となります。

list-09 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()を使うとこんなカンジ、あたかもスレッド内から飛んできた例外を直接捕まえてるように見えますね。

list-10 package_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がスレッドサポートライブラリに仲間入りしています。コレについてはまたいずれ、回を改めて紹介したいと思ってます。



  • LINEで送る
  • このエントリーをはてなブックマークに追加

バックナンバー

連載:C++11:スレッド・ライブラリひとめぐり

著者プロフィール

  • επιστημη(エピステーメー)

    C++に首まで浸かったプログラマ。 Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"り わんくま同盟でたまにセッションスピーカやったり 中国茶淹れてにわか茶人を気取ってたり、 あと Facebook とか。 著書: - STL標準...

あなたにオススメ

All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5