プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2017/08/17

async/await の動きを簡単なコードで確認してみる

update2017/10/30 event_note2017/08/17 0:28

C# 初心者が async/await について勉強しているといろいろ分からないことが出てくるので、出来る限りシンプルなコードで確認してみました。

私の理解と疑問点

  • 非同期メソッド(async が付いているメソッド)は await で処理を待つことができる
  • await を使用しているメソッドには async を付ける必要がある
  • await を付けなければ待たないこともできるが、警告が表示される
  • だから警告を消すために await をつけ、メソッドには async を付けよう
  • 同じ理由で呼び出し元のメソッドでも async/await をつける必要が出てくる
  • async/await が連鎖的に伝播していく
  • どこで終わればいいの?

① await しない場合

検証コード

namespace AsyncAwaitTest
{
  class Program
  {
    static void Main(string[] args)
    {
      Run();

      Console.WriteLine("[キー入力待ち]");
      Console.ReadKey();
    }

    static void Run()
    {
      Console.WriteLine("コール前");
      Hoge();
      Console.WriteLine("コール後");
    }

    // 何か重い処理を行うメソッド
    async static Task<bool> Hoge()
    {
      Console.WriteLine("Hoge 開始");
      await Task.Delay(3000);
      Console.WriteLine("Hoge 終了");
      return false;
    }
  }
}
  • Hoge() をコールしている箇所で警告が表示される

実行結果

コール前
Hoge 開始
コール後
[キー入力待ち]
Hoge 終了
  • await しないので、Hoge をコールした後すぐに制御が戻り「コール後」が出力されている
  • Hoge メソッドでは、await した箇所で完了を待ち、3秒経過後に次の処理へ進む

② await する場合

検証コード

namespace AsyncAwaitTest
{
  class Program
  {
    static void Main(string[] args)
    {
      Run();

      Console.WriteLine("[キー入力待ち]");
      Console.ReadKey();
    }

    async static void Run()
    {
      Console.WriteLine("コール前");
      await Hoge();
      Console.WriteLine("コール後");
    }

    // 何か重い処理を行うメソッド
    async static Task<bool> Hoge()
    {
      Console.WriteLine("Hoge 開始");
      await Task.Delay(3000);
      Console.WriteLine("Hoge 終了");
      return false;
    }
  }
}
  • 警告は表示されなくなる

実行結果

コール前
Hoge 開始
[キー入力待ち]
Hoge 終了
コール後
  • Run では Hoge を await するので、Hoge をコールした箇所で完了を待つようになる
  • Hoge でも await しているので、3秒経過するのを待つ
  • 制御が Main に戻る
  • 3秒経過後に Hoge の続きの処理が行われる
  • Hoge 完了後、Run の続きの処理が行われる

③ await して戻り値を取得する場合

検証コード

namespace AsyncAwaitTest
{
  class Program
  {
    static void Main(string[] args)
    {
      Run();

      Console.WriteLine("[キー入力待ち]");
      Console.ReadKey();
    }

    async static void Run()
    {
      Console.WriteLine("コール前");
      bool ret = await Hoge();
      Console.WriteLine("コール後");
    }

    // 何か重い処理を行うメソッド
    async static Task<bool> Hoge()
    {
      Console.WriteLine("Hoge 開始");
      await Task.Delay(3000);
      Console.WriteLine("Hoge 終了");
      return false;
    }
  }
}
  • 戻り値を取得しているだけで②と同じ

実行結果

②と同じ

結論

async/await が連鎖的に伝播していく
どこで終わればいいの?

に対する答えは async void のメソッドで終われば良い です。

しかし、async void はイベントハンドラなどでのみ使用すべきで、通常は、

  • 戻り値がない場合は async Task
  • 戻り値がある場合は async Task<T>

を使用すべきだそうです(参考 URL を参照)。

まとめると、

  • async void 以外のメソッドは必ず await しなければならない
  • await しなかったら警告が表示される
  • でも async void は基本的に使用してはダメ
  • async void は戻り値に取得できない(非同期処理を投げっぱなしで行う)
  • 唯一の例外がイベントハンドラであり、これは UI スレッドなどを止めないため
  • イベントハンドラ以外の処理を async void で行うのは、あまり使い道もなくはまりどころになるだけなのでやらないほうがよい
  • 従って、イベントハンドラまでは async/await を書き続ける必要がある

という感じでしょうか。

同期コンテキストとかの話は難しそうだったので、これから勉強します。

参考URL