晴耕雨読

working in the fields on fine days and reading books on rainy days

[C#] マルチスレッド処理:例外処理とAggregateException

マルチスレッド処理において、Task内で発生した例外はawaitを使うと例外をそのまま再スローします。 awaitではなく、Task#Waitメソッドを使うとAggregateExceptionに包まれて例外がスローされます。

Task#Waitの場合(AggregateException)

Task.Wait()メソッドで例外が発生した場合、スローされるのは AggregateException です。 AggregateException の InnerException プロパティには、実行中のタスク内で発生した例外が格納されます。 具体的には以下のようになります:

  • AggregateException.InnerExceptions(コレクション)には、タスク内で発生したすべての例外が格納されます。
  • AggregateException.InnerException(単一の例外)には、InnerExceptions の最初の例外(InnerExceptions[0])が格納されます。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;

class WaitExceptionExample
{
    public static void Main()
    {
        try
        {
            Task task = SomeMethod();
            task.Wait();
        }
        catch (AggregateException ex)
        {
            Console.Error.WriteLine($"[!] Error: {ex.InnerException?.GetType()}: {ex.InnerException?.Message}");
            // => [!] Error: System.NotImplementedException: 未実装です!
        }
    }

    public static async Task SomeMethod()
    {
        await Task.Yield();
        throw new NotImplementedException("未実装です!");
    }
}

awaitの場合

await は非同期メソッドの中で発生した個々の例外をそのまま再スローします。 そのため、try-catch ブロックで await を囲むと、直接その例外型(例:InvalidOperationExceptionなど)として捕捉できます。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;

class AwaitExceptionExample
{
    public static async Task Main()
    {
        try
        {
            await OtherMethod();
        }
        catch (NotImplementedException ex)
        {
            Console.Error.WriteLine($"[!] Error: {ex.GetType()}: {ex.Message}");
            // => [!] Error: System.NotImplementedException: 未実装です!
            throw;
        }
    }

    public static async Task OtherMethod()
    {
        await Task.Yield();
        throw new NotImplementedException("未実装です!");
    }
}

ただし、await Task.WhenAll(...) などの複数タスクをawaitした場合は、AggregateExceptionに例外が集約されてスローされます。

AggregateException#Handle

Task.Waitメソッドを使用する際には、非同期操作中に発生した例外がすべてAggregateExceptionとしてまとめてスローされる点に注意が必要です。 例外AggregateExceptionは、非同期タスク内で発生した複数の例外を1つに集約するため、例外処理を行う際には内部のInnerExceptionsコレクションを適切に確認・処理する必要があります。 処理するときは AggregateException#Handle メソッドを使うことで、例外を1つずつ処理することができます。

using System;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;

class StartAsyncAwaitExample
{
    public static void Main()
    {
        Task[] ts = new Task[3];
        ts[0] = Task.Run(() => { throw new Exception("Sample Error 1"); });
        ts[1] = Task.Run(() => { throw new Exception("Sample Error 2"); });
        ts[2] = Task.Run(() => { throw new Exception("Sample Error 3"); });
        Task? t = null;
        try
        {
            t = Task.WhenAll(ts);
            t.Wait();
        }
        catch (AggregateException ae)
        {
            ae.Handle((ex) =>
            {
                Console.Error.WriteLine($"[!] Error: {ex.GetType()}: {ex.Message}");
                return true;  // falseを返すとAggregateExceptionが親メソッドに伝播する
            });
        }
        // => [!] Error: System.Exception: Sample Error 1
        // => [!] Error: System.Exception: Sample Error 2
        // => [!] Error: System.Exception: Sample Error 3
    }
}

AggregateException#Handle メソッドのラムダ式の中で、falseを返すとAggregateExceptionが親メソッドに伝播します。 trueを返すとAggregateExceptionの例外が握り潰されます。

以上です。