1. はじめに
Task クラスや非同期メソッド(async/await)を使うと、例外は普通に try-catch で await の周りに置くか、ContinueWith を使って捕まえられます。例外は「失われず」、呼び出し元のスレッドに返ってきます。
しかし、Thread でスレッドを作成すると話が複雑になります。各スレッドは独自のエントリポイント(ThreadStart)と実行コンテキストを持っています。スレッド内で未処理の例外が発生すると、それはメインスレッドに「戻る」わけではなく、そのスレッドだけでスローされます。
- .NET Framework:あるスレッドの未処理例外はアプリケーション全体を終了させる。
- .NET (Core/5+):そのスレッドだけが終了し、アプリケーションは継続する(これが隠れたバグにつながることがある)。
結論:スレッド内の例外をキャッチしないと、たぶん気づかないままになります。だからスレッドでのエラー処理は必須です。
豆知識: Thread から飛び出した例外は捕まえにくい忍者のようなものです:消えてしまい、あとでなぜロジックが動かなかったのか頭を悩ませます。
2. スレッド内で例外はどう動くのか?
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
// 結果を見るためにスレッドの終了を待つ
thread.Join();
Console.WriteLine("Main は正常に終了しました");
}
static void DoWork()
{
Console.WriteLine("別スレッドで作業中...");
throw new Exception("ヤバい!スレッドでエラーが発生しました。");
}
}
プラットフォーム(古い .NET Framework か現代の .NET Core/5/6/7/8/9)によって振る舞いは変わります:アプリ全体が落ちるか、スレッドだけが落ちるか。ただ重要なのは — 例外はメインスレッドに届かないので、外側で処理できないという点です。
重要! thread.Join() を try-catch で囲んでも、別スレッドの例外は捕まえられません — その例外はスレッド内で「生きて」「死に」ます。
3. Thread で例外をどう捕まえるべきか?
スレッドに渡す関数の内部でだけです。例外を投げる可能性があるものは全部 try-catch で包んでください。
static void DoWork()
{
try
{
Console.WriteLine("作業中...");
throw new Exception("また何かがうまくいかなかった!");
}
catch (Exception ex)
{
Console.WriteLine($"[スレッド] 例外をキャッチしました: {ex.Message}");
// ここでログを残したり、UI/サーバーに送ったりする
}
}
スレッド内の例外処理はスレッド自身の責任です。呼び出し側のコードが自動的にエラーをキャッチしてくれるとは期待できません。
4. メインスレッドで、別スレッドで何か問題が起きたことをどう知るか?
実際のアプリではエラー情報をメインスレッドに届けることが重要です。
- スレッドセーフな仕組み(例えば ConcurrentQueue<Exception>)を使って、スレッドから例外を渡す。
- 作業スレッドからイベント/デリゲートを上げる。
- 基本的には Task を優先するのが楽です。Task は例外を await まで運んでくれます。
例:エラーを専用の場所に集める
using System;
using System.Threading;
class Program
{
static Exception? threadException = null;
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
thread.Join();
if (threadException != null)
{
Console.WriteLine($"別スレッドでエラーが発生しました: {threadException.Message}");
}
else
{
Console.WriteLine("スレッドはエラーなく終了しました。");
}
}
static void DoWork()
{
try
{
throw new Exception("別スレッドでトラブル!");
}
catch (Exception ex)
{
threadException = ex;
}
}
}
補足:この方法は同期的に待つ(Join())場合に使えます。スレッドが「自分の時間で」動いている場合やエラーが多発する場合は、ConcurrentQueue<Exception>、イベント、他のコミュニケーション手段を使ってください。
5. Task と比較:なぜエラー処理が楽なのか
async Task FooAsync()
{
throw new Exception("Task内のエラー!");
}
try
{
await FooAsync();
}
catch (Exception ex)
{
Console.WriteLine($"エラーをキャッチしました: {ex.Message}");
}
ここでは透明性があります:エラーは await される場所まで「届きます」。クラシックな Thread ではエラーはスレッド内に残り、特別な処理をしないと上位に伝わりません。これが Task や現代的な抽象化を使う利点の一つです。
6. 実践例
UI アプリ(WPF/WinForms)では、UI をブロックしないためにスレッドを使います。例外を処理しないと「灰色画面」や不思議なフリーズを招きます。
悪い例(エラー処理のないスレッド)
Thread thread = new Thread(() =>
{
// 長い処理
Thread.Sleep(5000);
throw new Exception("すべてがダメだ!"); // 誰もキャッチしない
});
thread.Start();
良い例(エラーをキャッチしてユーザーに知らせる)
Thread thread = new Thread(() =>
{
try
{
Thread.Sleep(5000);
throw new Exception("何か問題がある");
}
catch (Exception ex)
{
// MessageBox を表示したり、ログを残したり、UI に転送したりできる
Console.WriteLine($"スレッド内のエラー: {ex.Message}");
}
});
thread.Start();
7. 便利なポイント
スレッド未処理例外用のグローバルフック
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Console.WriteLine($"グローバルにエラーをキャッチしました: {((Exception)args.ExceptionObject).Message}");
};
Thread thread = new Thread(() =>
{
throw new Exception("エクスターミネイタス!");
});
thread.Start();
例外処理の違い — Thread vs Task
|
|
|
|---|---|---|
| どこで捕まえるか | スレッド内部 | 呼び出し側(await, ContinueWith 等) |
| 影響 | 例外が失われる/スレッドを殺す(または .NET Framework ではアプリ全体を終了させる) | 例外は待機する場所(await)まで届く |
| 上位への通知 | 明示的にのみ(変数、イベント、キュー) | await を通じて、同期的な待機では AggregateException を介して |
| ロギング | スレッド内で手動で行う必要がある | 通常は try-catch を await の周りに置く |
| コンテキスト | 親スレッドのコンテキストに依存しない | Task は呼び出し元の同期コンテキスト(例えば WPF の UI コンテキスト)を使用する |
8. Thread を使うときの典型的なミス
ミスその1:スレッド内の例外を捕まえない。
結果としてアプリの一部、場合によってはプロセス全体が不意に終了し、診断が難しくなる。
ミスその2:メインスレッドで別スレッドの例外を「捕まえようとする」こと。
これは動きません:try-catch で thread.Join() や thread.Start() を囲んでも、スレッド内でスローされた例外は捕まりません。
ミスその3:エラー情報を失う。
スレッドが落ちて、例外を明示的に渡していなければ(変数、キュー、イベント)、原因や詳細がわからなくなります。これが「幽霊バグ」につながります。
ミスその4:ロギングをしない。
スレッド内のエラーは必ずログに残してください。たとえ「大したことない」と思っても、後で役に立ちます。
GO TO FULL VERSION