1つのスレッドが呼び出す関数があります(これをメインスレッドと呼びます)。関数の本体内で、複数のワーカースレッドを生成してCPU集中型の作業を行い、すべてのスレッドが終了するのを待ってから、メインスレッドで結果を返します。
その結果、呼び出し元は関数を単純に使用でき、内部的には複数のコアを使用します。
これまでのところ良い..
私たちが抱えている問題は、例外を扱うことです。ワーカースレッドの例外によってアプリケーションがクラッシュすることは望ましくありません。関数の呼び出し元がメインスレッドでそれらをキャッチできるようにする必要があります。ワーカースレッドで例外をキャッチし、メインスレッドに伝播して、そこからアンワインドを継続する必要があります。
これをどのように行うことができますか?
私が考えることができる最高は:
これには、限られた例外タイプのセットしかサポートしないという明らかな欠点があり、新しい例外タイプが追加されるたびに変更が必要になります。
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
を保持する必要があるでしょう。
exception_ptr
は共有ptrのようなポインターであるため、少なくとも1つのexception_ptr
が各例外を指しているか、それらが解放されることに注意してください。
マイクロソフト固有:SEH例外(/EHa
)を使用する場合、サンプルコードはアクセス違反などのSEH例外も転送しますが、これは望ましくない場合があります。
現在、唯一のportable方法は、スレッド間で転送したいすべてのタイプの例外に対してcatch句を記述し、そこから情報を保存することです。 catch句を使用し、後でそれを使用して例外を再スローします。これは、 Boost.Exception がとるアプローチです。
C++ 0xでは、catch(...)
を使用して例外をキャッチし、std::current_exception()
を使用して_std::exception_ptr
_のインスタンスに保存できます。その後、std::rethrow_exception()
を使用して、同じスレッドまたは別のスレッドから後で再スローできます。
Microsoft Visual Studio 2005以降を使用している場合、 just :: thread C++ 0xスレッドライブラリ は_std::exception_ptr
_をサポートします。 (免責事項:これは私の製品です)。
C++ 11を使用している場合、std::future
はまさにあなたが探していることをするかもしれません:ワーカースレッドの最上部に到達する例外を自動的にトラップし、その時点で親スレッドに渡すことができますstd::future::get
が呼び出されます。 (舞台裏では、これは@AnthonyWilliamsの答えとまったく同じように発生します。すでに実装されています。)
欠点は、std::future
を「気にするのをやめる」標準的な方法がないことです。そのデストラクタでさえ、タスクが完了するまで単純にブロックします。 [EDIT、2017:ブロッキングデストラクタの動作は、std::async
から返された擬似未来の誤機能のみとにかく使用しないでください。通常の先物はデストラクタでブロックしません。しかし、std::future
を使用している場合、タスクを「キャンセル」することはできません。答えを誰も聞いていない場合でも、約束を果たすタスクはバックグラウンドで実行され続けます。]私が意味することを明確にするかもしれない例:
#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::thread
と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年に私が何を間違えていたのかわかりませんが、間違いだと思います。]
問題は、おそらく異なる理由から、それぞれが失敗する可能性があるため、複数のスレッドから複数の例外を受け取る可能性があることです。
メインスレッドは、スレッドが結果を取得するのを終了するのを何らかの形で待っているか、他のスレッドの進行状況を定期的にチェックし、共有データへのアクセスが同期されていると想定しています。
簡単な解決策は、各スレッドですべての例外をキャッチし、それらを共有変数(メインスレッド内)に記録することです。
すべてのスレッドが終了したら、例外をどう処理するかを決定します。これは、他のすべてのスレッドが処理を続行したことを意味しますが、これはおそらく望んでいないことです。
より複雑なソリューションは、別のスレッドから例外がスローされた場合、実行の戦略的ポイントで各スレッドをチェックすることです。
スレッドが例外をスローした場合、スレッドを終了する前にキャッチされ、例外オブジェクトはメインスレッドのコンテナーにコピーされ(単純なソリューションのように)、共有ブール変数がtrueに設定されます。
そして、別のスレッドがこのブール値をテストすると、実行が中止されることを確認し、適切な方法で中止します。
すべてのスレッドが中断した場合、メインスレッドは必要に応じて例外を処理できます。
スレッドからスローされた例外は、親スレッドではキャッチできません。スレッドには異なるコンテキストとスタックがあり、通常、親スレッドはそこにとどまり、子が終了するのを待つ必要がないため、例外をキャッチできます。そのキャッチのコードには、単に場所がありません。
try
{
start thread();
wait_finish( thread );
}
catch(...)
{
// will catch exceptions generated within start and wait,
// but not from the thread itself
}
各スレッド内で例外をキャッチし、メインスレッド内のスレッドからの終了ステータスを解釈して、必要な例外を再スローする必要があります。
ところで、スレッドのキャッチがない場合は、スタックの巻き戻しがまったく行われるかどうかは実装固有です。つまり、自動変数のデストラクタは、終了が呼び出される前に呼び出されることさえありません。一部のコンパイラはこれを行いますが、必須ではありません。
ワーカースレッドで例外をシリアル化し、それをメインスレッドに送信し、逆シリアル化し、再度スローできますか?これが機能するためには、例外はすべて同じクラスから派生する必要があります(または、少なくともswitchステートメントを含むクラスの小さなセット)。また、それらが直列化可能かどうかはわかりませんが、大声で考えています。
実際、あるスレッドから次のスレッドに例外を送信するための適切で一般的な方法はありません。
当然のことながら、すべての例外がstd :: exceptionから派生している場合、トップレベルの一般的な例外キャッチを使用して、例外をメインスレッドに送信し、そこで再度スローすることができます。問題は、例外のスローポイントを失うことです。この情報を取得して送信するために、おそらくコンパイラ依存のコードを書くことができます。
すべての例外がstd :: exceptionを継承しない場合、問題が発生し、スレッドに多くのトップレベルのキャッチを記述する必要があります...しかし、解決策はまだ有効です。
ワーカー内のすべての例外(アクセス違反などの非std例外を含む)の一般的なキャッチを実行し、ワーカースレッドからメッセージを送信する必要があります(ある種のメッセージングがあると仮定しますか?)。例外へのライブポインタを含むスレッド。例外のコピーを作成してそこに再スローします。その後、ワーカーは元のオブジェクトを解放して終了できます。
http://www.boost.org/doc/libs/release/libs/exception/doc/tutorial_exception_ptr.html を参照してください。子スレッドを結合するために呼び出す関数のラッパー関数を作成することもできます。これは、子スレッドによって発行された例外を(boost :: rethrow_exceptionを使用して)自動的に再スローします。