スレッド間で例外を伝播するにはどうすればよいですか?


105

単一のスレッドが呼び出す関数があります(これをメインスレッドと呼びます)。関数の本体内で、CPU集中型の作業を行うために複数のワーカースレッドを生成し、すべてのスレッドが完了するのを待ってから、メインスレッドで結果を返します。

その結果、呼び出し元は関数を単純に使用でき、内部的には複数のコアを使用します。

これまでのところすべて良い

私たちが抱えている問題は、例外を扱うことです。ワーカースレッドで例外を発生させてアプリケーションをクラッシュさせたくありません。メインスレッドでそれらをキャッチできるように、関数の呼び出し元が必要です。ワーカースレッドで例外をキャッチし、それらをメインスレッドに伝達して、そこから巻き戻しを続ける必要があります。

どうすればこれを行うことができますか?

私が考えることができる最高のものは:

  1. ワーカースレッドでさまざまな例外をキャッチします(std :: exceptionおよびいくつかの独自の例外)。
  2. 例外のタイプとメッセージを記録します。
  3. メインスレッドに、ワーカースレッドに記録されたタイプの例外を再スローする、対応するswitchステートメントを用意します。

これには、例外タイプの制限されたセットのみをサポートするという明らかな欠点があり、新しい例外タイプが追加されるたびに変更が必要になります。

回答:


89

C ++ 11では、exception_ptrスレッド間で例外を転送できるタイプが導入されました。

#include<iostream>
#include<thread>
#include<exception>
#include<stdexcept>

static std::exception_ptr teptr = nullptr;

void f()
{
    try
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        throw std::runtime_error("To be passed between threads");
    }
    catch(...)
    {
        teptr = std::current_exception();
    }
}

int main(int argc, char **argv)
{
    std::thread mythread(f);
    mythread.join();

    if (teptr) {
        try{
            std::rethrow_exception(teptr);
        }
        catch(const std::exception &ex)
        {
            std::cerr << "Thread exited with exception: " << ex.what() << "\n";
        }
    }

    return 0;
}

あなたのケースでは複数のワーカースレッドがあるのでexception_ptr、それぞれに1つ保持する必要があります。

exception_ptrあなたは、少なくとも1つの維持する必要がありますので、共有PTR-のようなポインタであるexception_ptr各例外にポインティングするか、彼らがリリースされます。

Microsoft固有:SEH例外(/EHa)を使用する場合、サンプルコードはアクセス違反などのSEH例外も転送しますが、これは意図したものとは異なる場合があります。


メインから生成された複数のスレッドはどうですか?最初のスレッドが例外にヒットして終了した場合、main()は、永久に実行される可能性がある2番目のスレッドのjoin()で待機します。main()は、2つのjoins()の後でteptrをテストすることはありません。すべてのスレッドが定期的にグローバルteptrをチェックし、適切な場合は終了する必要があるようです。この状況を処理するクリーンな方法はありますか?
コスモ

75

現在、移植可能な唯一の方法は、スレッド間で転送する可能性のあるすべてのタイプの例外のキャッチ節を記述し、そのキャッチ節のどこかに情報を格納し、後でそれを使用して例外を再スローすることです。これはBoost.Exceptionが採用するアプローチです。

C ++ 0xでは、を使用して例外をキャッチしcatch(...)、それをstd::exception_ptrを使用するインスタンスに格納できますstd::current_exception()。その後、同じスレッドまたは別のスレッドからで再スローできますstd::rethrow_exception()

Microsoft Visual Studio 2005以降を使用している場合は、just :: thread C ++ 0xスレッドライブラリがをサポートしますstd::exception_ptr。(免責事項:これは私の製品です)。


7
これは現在C ++ 11の一部であり、MSVS 2010でサポートされています。msdn.microsoft.com/en-us/library/dd293602.aspxを参照してください
JohanRåde12年

7
Linuxのgcc 4.4以降でもサポートされています。
アンソニーウィリアムズ

クール、使用例のリンクがあります:en.cppreference.com/w/cpp/error/exception_ptr
Alexis Wilke

11

あなたがC ++ 11を使用している場合は、std::futureそれが自動的にトラップ例外がワーカースレッドの一番上にそれを作ることができる、との点で親スレッドに介してそれらを渡す:あなたが探しているまさに行う可能性がありますstd::future::getです呼ばれた。(舞台裏では、これは@AnthonyWilliamsの回答とまったく同じように発生します。すでに実装されているだけです。)

欠点は、「気にしないでください」という標準的な方法がないことstd::futureです。そのデストラクタでさえ、タスクが完了するまでブロックするだけです。[編集、2017:ブロッキングデストラクタの動作は、から返される疑似フューチャーのみの誤機能あり、std::async決して使用しないでください。通常の先物はデストラクタでブロックされません。あなたが使用している場合しかし、あなたはまだタスク「キャンセル」することはできませんstd::future:誰ももう答えを聞いていない場合であっても裏で実行し続ける約束-充実したタスク(複数可)]ここで何を明らかにする可能性があるおもちゃの例ですI平均:

#include <atomic>
#include <chrono>
#include <exception>
#include <future>
#include <thread>
#include <vector>
#include <stdio.h>

bool is_prime(int n)
{
    if (n == 1010) {
        puts("is_prime(1010) throws an exception");
        throw std::logic_error("1010");
    }
    /* We actually want this loop to run slowly, for demonstration purposes. */
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    for (int i=2; i < n; ++i) { if (n % i == 0) return false; }
    return (n >= 2);
}

int worker()
{
    static std::atomic<int> hundreds(0);
    const int start = 100 * hundreds++;
    const int end = start + 100;
    int sum = 0;
    for (int i=start; i < end; ++i) {
        if (is_prime(i)) { printf("%d is prime\n", i); sum += i; }
    }
    return sum;
}

int spawn_workers(int N)
{
    std::vector<std::future<int>> waitables;
    for (int i=0; i < N; ++i) {
        std::future<int> f = std::async(std::launch::async, worker);
        waitables.emplace_back(std::move(f));
    }

    int sum = 0;
    for (std::future<int> &f : waitables) {
        sum += f.get();  /* may throw an exception */
    }
    return sum;
    /* But watch out! When f.get() throws an exception, we still need
     * to unwind the stack, which means destructing "waitables" and each
     * of its elements. The destructor of each std::future will block
     * as if calling this->wait(). So in fact this may not do what you
     * really want. */
}

int main()
{
    try {
        int sum = spawn_workers(100);
        printf("sum is %d\n", sum);
    } catch (std::exception &e) {
        /* This line will be printed after all the prime-number output. */
        printf("Caught %s\n", e.what());
    }
}

std::threadand を使用して同様の例を書こうとしstd::exception_ptrたが、std::exception_ptr(libc ++を使用して)何かがうまくいかなかったので、まだ実際に動作するようになっていない。:(

[編集、2017:

int main() {
    std::exception_ptr e;
    std::thread t1([&e](){
        try {
            ::operator new(-1);
        } catch (...) {
            e = std::current_exception();
        }
    });
    t1.join();
    try {
        std::rethrow_exception(e);
    } catch (const std::bad_alloc&) {
        puts("Success!");
    }
}

2013年に何が悪かったのかはわかりませんが、それは私のせいだったと思います。]


なぜという名前に未来を作成割り当てないf当時とemplace_backは?あなたはただやってみませんwaitables.push_back(std::async(…));か、それとも私は何かを見落としていませんか?
Konrad Rudolph

1
また、スタックするのではなく、フューチャーを中止することでスタックを巻き戻す方法はありwaitますか?「仕事の1つが失敗するとすぐに、他の人はもう問題ではなくなります」という線に沿った何か。
Konrad Rudolph

4年後、私の答えは熟成していません。:) Re "Why"について:明確にするためだと思います(async何か他のものではなく未来を返すことを示すため)。Re "Also、is there":ではありませんがstd::future、初心者向けにSTL全体を書き直してもかまわない場合の実装方法については、Sean Parentの講演「Better Code:Concurrency」または私の「Futures from Scratch」を参照してください。:)重要な検索用語は「キャンセル」です。
Quuxplusone 2017

お返事をありがとうございます。会ったら会談は必ず見ておきます。
Konrad Rudolph

1
良い2017編集。受け入れられたものと同じですが、スコープ付きの例外ポインターを持ちます。私はそれを一番上に置き、多分残りを取り除くでしょう。
Nathan Cooper、

6

問題は、おそらくさまざまな理由から、それぞれが失敗する可能性があるため、複数のスレッドから複数の例外を受け取る可能性があることです。

メインスレッドが結果を取得するためにスレッドが終了するのを何らかの形で待機している、または他のスレッドの進行状況を定期的に確認していると想定しています。共有データへのアクセスは同期されています。

シンプルなソリューション

単純な解決策は、各スレッドですべての例外をキャッチし、それらを(メインスレッドの)共有変数に記録することです。

すべてのスレッドが終了したら、例外をどう処理するかを決定します。これは、他のすべてのスレッドが処理を継続したことを意味します。

複雑なソリューション

より複雑な解決策は、別のスレッドから例外がスローされた場合に、実行の戦略的なポイントで各スレッドをチェックすることです。

スレッドが例外をスローすると、スレッドが終了する前にキャッチされ、例外オブジェクトがメインスレッドのコンテナーにコピーされ(単純なソリューションの場合と同様)、共有ブール変数がtrueに設定されます。

そして、別のスレッドがこのブール値をテストすると、実行が中止されることがわかり、適切な方法で中止されます。

すべてのスレッドが異常終了した場合、メインスレッドは必要に応じて例外を処理できます。


4

スレッドからスローされた例外は、親スレッドではキャッチできません。スレッドには異なるコンテキストとスタックがあり、通常、親スレッドはそこに留まり、子が完了するのを待つ必要がないため、例外をキャッチできます。そのキャッチのためのコードの場所は単にありません:

try
{
  start thread();
  wait_finish( thread );
}
catch(...)
{
  // will catch exceptions generated within start and wait, 
  // but not from the thread itself
}

各スレッド内で例外をキャッチし、メインスレッド内のスレッドからの終了ステータスを解釈して、必要な例外を再スローする必要があります。

ところで、スレッドにキャッチが存在しない場合、スタックの巻き戻しが行われるかどうかは実装固有です。つまり、終了が呼び出される前に自動変数のデストラクタが呼び出されない場合もあります。一部のコンパイラはそれを行いますが、必須ではありません。


3

ワーカースレッドで例外をシリアル化し、それをメインスレッドに送信して、逆シリアル化し、再度スローできますか?これが機能するためには、すべての例外が同じクラス(または少なくともswitchステートメントを持つクラスの小さなセット)から派生する必要があると思います。また、それらがシリアライズ可能かどうかはわかりませんが、大声で考えています。


両方のスレッドが同じプロセスにある場合、なぜそれをシリアル化する必要があるのですか?
Nawaz

1
@Nawazは、例外に他のスレッドが自動的に使用できないスレッドローカル変数への参照がある可能性があるためです。
tvanfosson

2

実際、あるスレッドから次のスレッドに例外を送信するための優れた一般的な方法はありません。

必要に応じて、すべての例外がstd :: exceptionから派生している場合は、トップレベルの一般的な例外キャッチを使用して、なんらかの理由でメインスレッドに例外を送信し、そこで再度スローすることができます。問題は、例外のスローポイントを失うことです。おそらく、コンパイラー依存のコードを記述して、この情報を取得して送信することもできます。

すべての例外がstd :: exceptionを継承しない場合、問題が発生し、スレッドに多くのトップレベルのキャッチを記述する必要がありますが、解決策はまだ保持されています。


1

ワーカー内のすべての例外(アクセス違反などのstd以外の例外を含む)の一般的なキャッチを実行し、ワーカースレッドからメッセージを送信する必要があります(何らかのメッセージングがあると思いますか?)スレッドは、例外へのライブポインタを含み、例外のコピーを作成してそこに再スローします。その後、ワーカーは元のオブジェクトを解放して終了できます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.