1. ホーム 
  2. 備忘録 
  3. C Sharp

Taskとasync/await

Taskオブジェクト

値を返さない 1 つの操作を表し、通常は非同期的に実行される

Taskオブジェクトは、.NET Framework 4 で最初に導入されたタスクベースの非同期パターンの中心的なコンポーネントの1つである

Taskオブジェクトそのものは非同期ではなくあくまで作業の単位であり、Task.Run() メソッドやその他Taskオブジェクトを動かす処理によって非同期が実現する

Taskオブジェクトを使って非同期処理を実装するメリット・デメリットは以下のようなものがある


    メリット

  • メインスレッドの進行を止めずに処理を進められる(画面フリーズを防げる)
  • マルチコアCPUでは処理時間短縮が見込める

  • デメリット

  • Task内での例外を補足しづらい(意識して処理するコードが必要)
  • デッドロックの発生
  • 並列アクセスによるスレッドセーフ問題
  • 理解が浅いときちんと非同期処理になっていないコードを書いてしまう
  • シングルコアCPUでは切り替え時間分、処理時間が伸びてしまう

Taskクラスを使った簡単な非同期処理の例を以下に記す

static void Main()
{
    var task1 = Task.Run( () => {
        Console.WriteLine( "Task1 開始" );
        Thread.Sleep( 2000 );
        Console.WriteLine( "Task1 終了" );
    } );

    var task2 = Task.Run( () => {
        Console.WriteLine( "Task2 開始" );
        Thread.Sleep( 3000 );
        Console.WriteLine( "Task2 終了" );
    } );

    Console.ReadLine();

    // 結果
    // タスクの開始タイミングは必ずしも先に書いたコードのものが早いというわけではないことに注意する
    // Task2 開始
    // Task1 開始
    // Task1 終了
    // Task2 終了
}

他にも色々な方法でタスクを開始することができる

static void Main()
{
    Action<object> action = ( object obj ) =>
    {
        Console.WriteLine( "Task={0}, obj={1}, Thread={2}",
        Task.CurrentId, obj,
        Thread.CurrentThread.ManagedThreadId );
    };

    // 生成のみ(実行しない)
    Task t1 = new Task( action, "alpha" );

    // タスク2を生成 & 実行
    Task t2 = Task.Factory.StartNew( action, "beta" );
    // タスク2の完了を待つ(メインスレッドが止まる)
    t2.Wait();

    // タスク1を実行
    t1.Start();
    Console.WriteLine( "t1 has been launched. (Main Thread={0})",
                      Thread.CurrentThread.ManagedThreadId );
    // タスク1の完了を待つ(メインスレッドが止まる)
    t1.Wait();

    // Task.Run() を使ったタスク3の生成 & 実行
    String taskData = "delta";
    Task t3 = Task.Run( () => {
        Console.WriteLine( "Task={0}, obj={1}, Thread={2}",
                                                 Task.CurrentId, taskData,
                                                  Thread.CurrentThread.ManagedThreadId );
    } );
    // タスク3の完了を待つ(メインスレッドが止まる)
    t3.Wait();

    // タスク4の生成
    Task t4 = new Task( action, "gamma" );
    // 同期的に実行する
    // つまり、処理が完了するまでメインスレッドが止まる
    t4.RunSynchronously();
    // 同期的に実行しているのでタスクを待つ必要はないが、
    // 例外がスローされた場合に備えて待機することを推奨している
    t4.Wait();

    // 結果
    // Task = 9, obj = beta, Thread = 5
    // Task = 10, obj = alpha, Thread = 5
    // t1 has been launched. ( Main Thread = 1)
    // Task = 11, obj = delta, Thread = 5
    // Task = 12, obj = gamma, Thread = 1
}

async/await

非同期処理しているタスクの完了を待つ方法として async/await がある

async キーワードはメソッドを非同期メソッドに変換し、メソッドの中で await キーワードを使用できるようにする

await キーワードは呼び出しメソッドを中断し、待機中のタスクが完了するまで呼び出し元に制御を戻す

async/await の使い方の例を以下に記す

using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        // 処理順を①~⑨で表す
        // 【注意!!】アプリケーション上での非同期処理実行時、スレッド処理完了後に元のスレッドに戻ろうとする
        //           しかし、元のスレッドで Wait() や Result でメソッドの完了を待っている状態だと
        //           お互いに待機することとなりデッドロックが発生する
        //           そのため、スレッド処理完了後に元のスレッドに帰る必要がない場合は、
        //           ConfigureAwait( false )にすることでこのデッドロックを避けることができる
        // 参照元 : 
        // https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming#%E3%81%99%E3%81%B9%E3%81%A6%E9%9D%9E%E5%90%8C%E6%9C%9F%E3%81%AB%E3%81%99%E3%82%8B

        // TestMethodAsync()を実行する … ①
        _ = TestMethodAsync();

        // TestMethod2Async()を実行して結果を待つ … ④
        var res = TestMethod2Async().Result;

        // メインスレッドのIDになっている … ⑨
        Console.WriteLine( res + " ThreadID:{0}", Thread.CurrentThread.ManagedThreadId );

        // 結果
        // method1 開始 ThreadID: 1
        // method2 開始 ThreadID: 1
        // method1 終了 ThreadID: 3
        // method2 終了 ThreadID: 5
        // test2 ThreadID:1
    }

    // 戻り値なしの非同期メソッド
    static async Task TestMethodAsync()
    {
        // ここはメインスレッドと同じスレッドID … ②
        Console.WriteLine( "method1 開始 ThreadID:{0}", Thread.CurrentThread.ManagedThreadId );

        // await を使うことでタスクの処理完了を待つことができる … ③
        // メソッドを中断し、タスクが完了するまで呼び出し元に制御を戻す
        await Task.Delay( 2000 ).ConfigureAwait( false );

        // TestMethod2Async() の中のタスクよりこちらが先に完了する … ⑦
        // メインスレッドとは異なるスレッドIDになっている
        Console.WriteLine( "method1 終了 ThreadID:{0}", Thread.CurrentThread.ManagedThreadId );
    }

    // 戻り値あり(string型)の非同期メソッド
    static async Task<string> TestMethod2Async()
    {
        // ここはメインスレッドと同じスレッドID … ⑤
        Console.WriteLine( "method2 開始 ThreadID:{0}", Thread.CurrentThread.ManagedThreadId );

        // await を使うことでタスクの処理完了を待つことができる … ⑥
        // メソッドを中断し、タスクが完了するまで呼び出し元に制御を戻す(ただし呼び出し元は Result で待機中なので動かない)
        string res = await Task.Run( () =>
        {
            Thread.Sleep( 4000 );
            return "test2";
        } ).ConfigureAwait( false );

        // メインスレッドとは異なるスレッドIDになっている … ⑧
        Console.WriteLine( "method2 終了 ThreadID:{0}", Thread.CurrentThread.ManagedThreadId );
        return res;
    }
}

なお、タスクのキャンセルや例外を検知したい場合は try~catch 構文を使用する

try
{
    res = await Task.Run( () =>
    {
        Thread.Sleep( 4000 );
        return "test2";
    } ).ConfigureAwait( false );
}
catch ( TaskCanceledException ex )
{
    // キャンセル時の例外処理
}
catch ( Exception ex ) 
{
    // 異常終了時の例外処理
}

    参考文献

  1. async/await を完全に理解する
  2. [C#]非同期メソッドの使い方 -Taskをawaitするasyncなメソッドです-