CodeGym /コース /C# SELF /クラシックな Thread における例...

クラシックな Thread における例外

C# SELF
レベル 61, レッスン 3
使用可能

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();

ハンドラはスレッド内の未処理例外に対して呼ばれますが、.NET Framework ではスレッドを復活させたりプロセスの終了を防ぐことはできません。.NET (Core/5+) ではエラーをログに残します;アプリケーションは他のスレッドが動いていれば継続する可能性があります。

例外処理の違い — Thread vs Task

Thread
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:ロギングをしない。
スレッド内のエラーは必ずログに残してください。たとえ「大したことない」と思っても、後で役に立ちます。

2
タスク
C# SELF, レベル 61, レッスン 3
ロック未解除
グローバル未処理例外ハンドラー
グローバル未処理例外ハンドラー
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION