[C# 5.0] async/await についての勉強メモ

勉強メモというか参考リンクが全て

参考リンク

以降は自分のメモなのであまり読む価値はないかも。つまずいたところなど。

メモ

  • async付きメソッドは中でawaitを使えるようになる。
  • async付きメソッドは中でawaitを使わなければ無意味。 つまりasyncを付けたから非同期になるわけじゃない。
  • awaitしても対象のTaskが完了状態の場合はそのawaitは非同期にならない。
  • async付きメソッドは一般的に Async というサフィックスを名前に付ける
  • async付きメソッドはawaitが出てくるまでは普通の関数と変わりはない。 awaitが出てきたところで非同期処理が実行され、呼び出し元に処理は戻る。
  • awaitの後続処理はどのスレッドで実行されるかは不定…だと思ったけど違った。 SynchronizationContextにより挙動が左右される大きな罠。
  • Task.Run(action)Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default) のshorthand
  • 覚えておくとよさげなクラスやメソッド
    • Task.WhenAll: Task.WaitAll はブロックするのでダメ
    • Concurrentコレクション: BlockingCollection, ConcurrentDictionary, ConcurrentQueue, ConcurrentStack
    • Task.Delay: Thread.Sleepはダメ
    • PLINQのためのAsParallel, AsOrdered, WithDegreeOfParallelism

Task.Run(...)var t = new Task(...); t.Start() の違い

Taskのコンストラクタで生成したタスクは async void 関数と同様だと思えばよい。 つまり最初の await でそのタスクは完了状態となる。 ※voidが返された時にawait後の完了状態をどうやって知るのかという話

Task.Runの戻り値のタスクは async Task であり、 これは非同期処理が全て完了したときに完了状態となる。 最初の await では完了状態とならず、await後の全ての処理を実行後に完了状態となる。

参考: c# - Task.Start strange behavoir - Stack Overflow

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

class Program
{
  static void Main(string[] args)
  {
    var stopWath = Stopwatch.StartNew();
    SubAsync().Wait();
    Console.WriteLine("elapsed time: {1}", Thread.CurrentThread.ManagedThreadId, stopWatch.Elapsed);
    Console.ReadLine();
  }

  static async Task SubAsync()
  {
    Console.WriteLine("BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

    // async void は await で待てない
    // (Task返ってこないので完了がわからない)ためコンパイルエラーになる
    await VoidAsync();

    //
    // しかし・・・
    //

    // TaskにはFunc<Task>を受け取るコンストラクタがない。
    // つまりTaskのコンストラクタで生成したタスクは`async void`扱いである。
    var task = new Task(async () =>
    {
      await Task.Delay(TimeSpan.FromSeconds(1));
    });
    task.Start();

    // これはコンパイルエラーにならない。
    // 実際に実行してみるとTaskのコンストラクタに指定したAction内のawaitのところで
    // task は完了状態となり1秒待たずに即座に次のコードが実行される。
    await task;
    Console.WriteLine("END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
  }

  static async void VoidAsync()
  {
    await Task.Delay(1);
  }
}

テスト1: コンソールアプリケーションでasync/await

こんなコンソールアプリケーションを実行してみる。 コンソールアプリケーションでは SynchronizationContext.Current が null なので注意。

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("Main  BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
    Program.SubAsync();
    Console.WriteLine("Main  END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

    Console.ReadLine();
  }

  static async void SubAsync()
  {
    Console.WriteLine("Sub   BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
    await Task.Run(async () =>
    {
      Console.WriteLine("Delay BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
      await Task.Delay(TimeSpan.FromSeconds(1));
      Console.WriteLine("Delay END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
    });
    Console.WriteLine("Sub   END.   ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
  }
}

結果の一例(ThreadIdは実行毎に変わる。"Main END"と"Delay BEGIN"は実行順が入れ替わることあり)

Main  BEGIN. ThreadId=10
Sub   BEGIN. ThreadId=10
Delay BEGIN. ThreadId=11
Main  END.   ThreadId=10
Delay END.   ThreadId=6
Sub   END.   ThreadId=6

つまり

class Program
{
  static void Main(string[] args)
  {
    1Console.WriteLine("Main  BEGIN. ThreadId=メインスレッド");
    // await していないのでSubAsyncの非同期処理の終わりを待たない
    Program.SubAsync();
    3 or 4Console.WriteLine("Main  END.   ThreadId=メインスレッド");

    Console.ReadLine();
  }

  static async Task SubAsync()
  {
    2Console.WriteLine("Sub   BEGIN. ThreadId=メインスレッド");
    await Task.Run(async () =>
    {
      3 or 4Console.WriteLine("Delay BEGIN. ThreadId=非同期スレッド1");
      await Task.Delay(TimeSpan.FromSeconds(1));
      5Console.WriteLine("Delay END.   ThreadId=非同期スレッド2");
    });

    // あまり気にする意味はないと思うけど
    // 非同期スレッド2と3は何度実行しても同じになった
    6Console.WriteLine("Sub   END.   ThreadId=非同期スレッド3");
  }
}

テスト2: 完了したtaskをawaitしても非同期処理にならない

class Program
{
  static void Main(string[] args)
  {
    SubAsync();
    Console.ReadLine();
  }

  static async Task SubAsync()
  {
    int threadId = Thread.CurrentThread.ManagedThreadId;
    var task = Task.Delay(TimeSpan.FromSeconds(1));

    Thread.Sleep(TimeSpan.FromSeconds(3));

    // "task is completed" となるはず
    Console.WriteLine("task is {0} completed",
      task.IsCompleted ? "" : "not");
    await task;

    // 前後でスレッドIDは変わらないはず
    Console.WriteLine("await前後のスレッドID: {0} - {1}",
      threadId, Thread.CurrentThread.ManagedThreadId);
  }
}
Share
関連記事