CodeGym /コース /C# SELF /「fire and forget」タスクでの例外

「fire and forget」タスクでの例外

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

1. 「fire and forget」って何?

プログラミングでの用語としてのfire and forgetは、タスクを開始してその完了を待たないことを意味します。C# / .NETの世界では、だいたいTaskを起動してどこでも待たない(awaitしない)、参照を保持しない、つまり実質的に忘れてしまう、というパターンです。

// ボタンがバックグラウンドでタスクを起動するが、どこでも await されない。
button.Click += (s, e) =>
{
    Task.Run(() => 長い処理());
};

一見便利です: 「バックグラウンドで動かしておけば自分は別のことをするだけ」。でもこのやり方だと、タスク内で例外が起きても誰もすぐには気づかない — 例外が静かに消えてしまいます。

2. Task の例外処理はどう動くか

基本: await とエラー処理

非同期タスクの標準的な扱い方は await を使うことです。タスク内でエラーが起きたら、それは待っている箇所に再スローされます:

try
{
    await SomeOperationAsync(); // ここで内部が Exception なら catch に入る
}
catch(Exception ex)
{
    Console.WriteLine("おっと!タスクでエラーが発生しました: " + ex.Message);
}

つまり、タスクを待てば例外を見逃しません。

でも「fire and forget」タスクは誰も待たない!

public void 待たずに開始()
{
    // タスクは単独で動く。誰も待っていない...
    Task.Run(() => {
        // どこかで問題が起きる:
        throw new InvalidOperationException("おっと、全部ダメだ!");
    });
    // メソッドは終了、タスクは静かにバックグラウンドで動作。
}

そのようなタスク内で例外が起きても、それはメインスレッドでは投げられない。アプリは何事もなかったかのように続行します。

重要なポイント

.NETでは、未処理の例外を含むタスクは状態が Faulted になります。しかしそのタスクを待たない(await.Result.Wait() 等をしない)と、例外は誰にも読まれず、呼び出し元のコードには現れません。

実際に「裏側」で何が起きているのか?

誰も待たないタスクが認識される最後のチャンスは、TaskScheduler.UnobservedTaskException イベントです。これは GC が未観測の例外を含むタスクを検出したときに発火します。でもそれは即時でも期待した場所でも起きないので、これに頼るのは危険です。

3. デモ: Fire-and-forget のエラー

// 例: Main からそのまま fire-and-forget タスクを起動
using System;
using System.Threading.Tasks;

class Program
{
    static void Main(string[] args)
    {
        FireAndForgetExample();

        Console.WriteLine("メインスレッドは動き続けます...");
        // タスクに終わる時間を少し与える
        Task.Delay(2000).Wait();
    }

    static void FireAndForgetExample()
    {
        Task.Run(() =>
        {
            Console.WriteLine("Fire-and-forget タスクが始まった!");
            Task.Delay(500).Wait();
            throw new InvalidOperationException("fire-and-forget タスク内のエラー!");
        });
    }
}

このコードを実行しても……特に何も起きないように見えるでしょう。例外は発生しますが、プログラムはそれを知らないままです。IDE の Output Window に警告が出ることはありますが、ユーザー側には何も伝わりません。

実運用でなぜ危険か?

  • 再現が難しい複雑なバグ(「たまに動かない、原因不明」)。
  • データやロジックが静かに失われる(例えばクライアントへのメールが送信されない)。
  • 本番環境でログが無ければ問題のシグナルが出ない。

4. fire-and-forget での正しいエラー処理方法

タスク内部でのログと例外処理

最低限の安全策は、fire-and-forget タスクの中で例外をキャッチすることです:

Task.Run(() =>
{
    try
    {
        // 長くて危険なコード
        throw new InvalidOperationException("何かがうまくいかなかった!");
    }
    catch (Exception ex)
    {
        // エラーをログするかユーザーに通知する
        Console.WriteLine("Fire-and-forget: 例外をキャッチしました: " + ex.Message);
        // ログファイルに書いたり、監視システムに送ったりできます
    }
});

非同期の void メソッド(なぜ避けるべきか)

async void DangerousFireAndForget()
{
    // 危険な何か
    throw new Exception("ドカン!");
}

async void メソッドは本質的に fire-and-forget です: 待てないし、Task を返さない。これらからの例外はアプリのグローバルハンドラ(例えば AppDomain.UnhandledException)に飛び、プロセスのクラッシュにつながることが多いです。イベントハンドラ以外では async void を使わないでください — どうしても使うなら注意深く。

エラーハンドリング用のヘルパーを使う

fire-and-forget の安全な起動をラッパーに切り出すと便利です:

// fire-and-forget を安全に起動する汎用メソッド
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
    Task.Run(async () =>
    {
        try
        {
            await taskFactory();
        }
        catch (Exception ex)
        {
            // 例外をログする
            Console.WriteLine("Fire-and-forget (safe): " + ex);
            // 監視システムへの送信なんかもここでできる
        }
    });
}

// 使用例:
RunSafeFireAndForget(async () =>
{
    await Task.Delay(1000);
    throw new InvalidOperationException("fire-and-forget 内部で!");
});

実例: メール送信

// 送信ボタン:
private void buttonSend_Click(object sender, EventArgs e)
{
    Task.Run(() => 送信処理());
}

// 送信メソッド:
private void 送信処理()
{
    try
    {
        // ここで実際の送信があるかもしれない
        throw new Exception("SMTP サーバーに接続できない!");
    }
    catch (Exception ex)
    {
        // ログ
        File.AppendAllText("errors.log", $"送信エラー: {ex.Message}\n");
    }
}

5. UnobservedTaskException はどうなの?

最後の手段として、.NET は TaskScheduler.UnobservedTaskException イベントを提供しています。これはタスクがエラーで終了し、誰も待たずにタスクオブジェクトが GC によって回収されたときに呼ばれます。これは「最後のチャンス」メカニズムなので、頼り切るのはおすすめしません。

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    Console.WriteLine("グローバルな UnobservedTaskException: " + e.Exception);
    e.SetObserved(); // これを呼ばないとアプリがクラッシュする可能性があるので忘れずに!
};

詳しくは: TaskScheduler.UnobservedTaskException.

6. 役立つ細かい注意点

アプローチの簡略比較

方法 例外は処理されるか どこでエラーを捕まえるか エラーを「失う」リスク
await
はい 呼び出し側のコード 低い
try/catch なしの Fire-and-forget いいえ どこでもない 非常に高い
try/catch 付きの Fire-and-forget はい タスク自体の内部 低い(ログすれば)
async void メソッド いいえ(グローバルへ飛ぶ) グローバルハンドラ 高い

fire-and-forget を設計するときの心得

  • タスクの結果や状態が重要なら、fire-and-forget にしないでください。await するか、後で待てるように Task を保持しましょう。
  • fire-and-forget は本当に重要でないバックグラウンド作業(例: テレメトリ送信)だけに限定してください。
  • 常に fire-and-forget を自前のメソッドでラップし、例外を捕まえてログを残しましょう。
  • 複雑なバックグラウンドシナリオにはキューやワーカーを使う: Hangfire, Quartz.NET など。

実務と面接での使いどころ

面接でよく聞かれるのは: 「fire-and-forget タスクで例外が起きたらどうなる?」とか「なんでどこでも async void を使ってはいけないの?」といった質問です。正しい答えは:バックグラウンドタスクのエラー運命はあなただけが管理する — 例外を捕まえてログし分析するか、幽霊バグを受け取るか、のどちらか、ということです。

「fire-and-forget」と await の対比

シナリオ エラー処理の信頼性 適用範囲
通常の await 優秀 結果が必要な場所や成功/失敗が重要な場所
Fire-and-forget 悪い(手動処理しないと) 本当にバックグラウンドで重要でないタスクだけ
try/catch 付きの Fire-and-forget 良い(ログすれば) 結果は不要だが障害を知りたいバックグラウンド処理

次の講義では、複数の結果を返す並列タスクでのエラー処理について話します。覚えておいてください: どこかに「発射」したら、ちゃんと届いたか確認しましょう!

7. fire-and-forget タスクでのよくあるミス

ミス №1: fire-and-forget の例外を無視すること。
初心者は例外が「どこかで浮上する」と期待しがちです。try-catch とログ無しでは、例外は消えて見つからないバグになります。

ミス №2: イベントハンドラ以外での async void の使用。
こうしたメソッドは例外をグローバルハンドラ(例えば AppDomain.UnhandledException)に投げ、アプリをクラッシュさせる可能性があります。

ミス №3: 過剰な例外処理。
タスク内部ですべてを捕まえすぎると、呼び出し側で適切に処理すべき問題まで隠してしまい、デバッグが難しくなります。

ミス №4: ログを怠ること。
fire-and-forget タスクでログが無ければ、本番での障害を知ることができません。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION