vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【C#】Task の話 その 4

はじめに

さて今回は、 async/await の話。

async/await

その 1 で登場したこのコード。

そのままだとメインスレッドの処理が先に終わってしまい、 Task.Run の中身が実行される前に処理が完了してしまう、という話を書きました。

Program.cs

using System;

namespace TaskSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            player.Play();
        }
    }
}

TaskSamplePlayer.cs

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

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public void Play()
        {
            Task.Run(() =>
            {
                Console.WriteLine("Hello world!");
            });
        }
    }
}

この程度の処理であれば Thread.Sleep などで少し待ってやれば OK な気もします。

ただ、もっと重い処理を別スレッドで行いたい場合、待ち時間より処理の時間がかかると先ほどと同じ結果が起こります。

これを処理が完了するのを待ってメインスレッドの処理が終わるようにする、という方法にはいくつかあり、その一つが async/await です。

※下記のコードは C# 7.1 以上で実行する必要があります。

Program.cs

using System;
using System.Threading.Tasks;

namespace TaskSample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var player = new TaskSamplePlayer();
            await player.PlayAsync();
            
            // player.Play() の処理が完了した後に実行される.
            Console.WriteLine("end");
        }
        
    }
}

TaskSamplePlayer.cs

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

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public async Task PlayAsync()
        {
            await Task.Run(() =>
            {
                Console.WriteLine("Hello world!");
            });
        }
    }
}

C# 7.1 から Main メソッドに async キーワードを付けられるようになりました。

これでシンプルに PlayAsync() の処理を待つことができます。

IL で比較する

スラスラわかる C# などによると、 async/await の内 async キーワード自体には何か機能があるわけではなく、 await を下位互換性を保ちながら導入するために必要なもの、という位置づけのようです。

ということは、 await を追いかけるとどのようなことをしているかがわかりそうです。

これを確かめるため、 ReSharper 先生の IL Viewer に力を借りることにします。

元のコード (TaskSamplePlayer.cs)

using System;
using System.Threading.Tasks;

namespace TaskSample
{
    public class TaskSamplePlayer
    {
        public async Task PlayAsync()
        {
            await Task.Run(() => { Console.WriteLine("hello"); });
        }
        public Task Play()
        {
            return Task.Run(() => { Console.WriteLine("hello");});
        }
    }
}

これの PlayAsync() と Play() を比較してみます。

PlayAsync()

// Type: TaskSample.TaskSamplePlayer 
// Assembly: TaskSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: XXXX
// Location: C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.dll
// Sequence point data from C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.pdb

.class public auto ansi beforefieldinit
  TaskSample.TaskSamplePlayer
    extends [System.Runtime]System.Object
{
  .class nested private sealed auto ansi serializable beforefieldinit
    '<>c'
      extends [System.Runtime]System.Object
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public static initonly class TaskSample.TaskSamplePlayer/'<>c' '<>9'

    .field public static class [System.Runtime]System.Action '<>9__0_0'

    .field public static class [System.Runtime]System.Action '<>9__1_0'

~省略~

    .method assembly hidebysig instance void
      'b__0_0'() cil managed
    {
      .maxstack 8

      // [10 34 - 10 35]
      IL_0000: nop

      // [10 36 - 10 63]
      IL_0001: ldstr        "hello"
      IL_0006: call         void [System.Console]System.Console::WriteLine(string)
      IL_000b: nop

      // [10 64 - 10 65]
      IL_000c: ret

    } // end of method '<>c'::'b__0_0'
~省略~
  } // end of class '<>c'

  .class nested private sealed auto ansi beforefieldinit
    'd__0'
      extends [System.Runtime]System.Object
      implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public int32 '<>1__state'

    .field public valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder '<>t__builder'

    .field public class TaskSample.TaskSamplePlayer '<>4__this'

    .field private valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter '<>u__1'

    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    {
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [System.Runtime]System.Object::.ctor()
      IL_0006: nop
      IL_0007: ret

    } // end of method 'd__0'::.ctor

    .method private final hidebysig virtual newslot instance void
      MoveNext() cil managed
    {
      .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext()
      .maxstack 3
      .locals init (
        [0] int32 V_0,
        [1] valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter V_1,
        [2] class TaskSample.TaskSamplePlayer/'d__0' V_2,
        [3] class [System.Runtime]System.Exception V_3
      )

      IL_0000: ldarg.0      // this
      IL_0001: ldfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
      IL_0006: stloc.0      // V_0
      .try
      {

        IL_0007: ldloc.0      // V_0
        IL_0008: brfalse.s    IL_000c
        IL_000a: br.s         IL_000e
        IL_000c: br.s         IL_0066

        // [9 9 - 9 10]
        IL_000e: nop

        // [10 13 - 10 67]
        IL_000f: ldsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__0_0'
        IL_0014: dup
        IL_0015: brtrue.s     IL_002e
        IL_0017: pop
        IL_0018: ldsfld       class TaskSample.TaskSamplePlayer/'<>c' TaskSample.TaskSamplePlayer/'<>c'::'<>9'
        IL_001d: ldftn        instance void TaskSample.TaskSamplePlayer/'<>c'::'b__0_0'()
        IL_0023: newobj       instance void [System.Runtime]System.Action::.ctor(object, native int)
        IL_0028: dup
        IL_0029: stsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__0_0'
        IL_002e: call         class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
        IL_0033: callvirt     instance valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter [System.Runtime]System.Threading.Tasks.Task::GetAwaiter()
        IL_0038: stloc.1      // V_1

        IL_0039: ldloca.s     V_1
        IL_003b: call         instance bool [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
        IL_0040: brtrue.s     IL_0082
        IL_0042: ldarg.0      // this
        IL_0043: ldc.i4.0
        IL_0044: dup
        IL_0045: stloc.0      // V_0
        IL_0046: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
        IL_004b: ldarg.0      // this
        IL_004c: ldloc.1      // V_1
        IL_004d: stfld        valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/'d__0'::'<>u__1'
        IL_0052: ldarg.0      // this
        IL_0053: stloc.2      // V_2
        IL_0054: ldarg.0      // this
        IL_0055: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
        IL_005a: ldloca.s     V_1
        IL_005c: ldloca.s     V_2
        IL_005e: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::AwaitUnsafeOnCompletedd__0'>(!!0/*valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter*/&, !!1/*class TaskSample.TaskSamplePlayer/'d__0'*/&)
        IL_0063: nop
        IL_0064: leave.s      IL_00b8
        IL_0066: ldarg.0      // this
        IL_0067: ldfld        valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/'d__0'::'<>u__1'
        IL_006c: stloc.1      // V_1
        IL_006d: ldarg.0      // this
        IL_006e: ldflda       valuetype [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter TaskSample.TaskSamplePlayer/'d__0'::'<>u__1'
        IL_0073: initobj      [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter
        IL_0079: ldarg.0      // this
        IL_007a: ldc.i4.m1
        IL_007b: dup
        IL_007c: stloc.0      // V_0
        IL_007d: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
        IL_0082: ldloca.s     V_1
        IL_0084: call         instance void [System.Runtime]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
        IL_0089: nop
        IL_008a: leave.s      IL_00a4
      } // end of .try
      catch [System.Runtime]System.Exception
      {

        IL_008c: stloc.3      // V_3
        IL_008d: ldarg.0      // this
        IL_008e: ldc.i4.s     -2 // 0xfe
        IL_0090: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
        IL_0095: ldarg.0      // this
        IL_0096: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
        IL_009b: ldloc.3      // V_3
        IL_009c: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetException(class [System.Runtime]System.Exception)
        IL_00a1: nop
        IL_00a2: leave.s      IL_00b8
      } // end of catch

      // [11 9 - 11 10]
      IL_00a4: ldarg.0      // this
      IL_00a5: ldc.i4.s     -2 // 0xfe
      IL_00a7: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'

      IL_00ac: ldarg.0      // this
      IL_00ad: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
      IL_00b2: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::SetResult()
      IL_00b7: nop
      IL_00b8: ret

    } // end of method 'd__0'::MoveNext

    .method private final hidebysig virtual newslot instance void
      SetStateMachine(
        class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
      ) cil managed
    {
      .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance void [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine(class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine)
      .maxstack 8

      IL_0000: ret

    } // end of method 'd__0'::SetStateMachine
  } // end of class 'd__0'

  .method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task
    PlayAsync() cil managed
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type)
      = (
        01 00 2b 54 61 73 6b 53 61 6d 70 6c 65 2e 54 61 // ..+TaskSample.Ta
        73 6b 53 61 6d 70 6c 65 50 6c 61 79 65 72 2b 3c // skSamplePlayer+<
        50 6c 61 79 41 73 79 6e 63 3e 64 5f 5f 30 00 00 // PlayAsync>d__0..
      )
      // type(class TaskSample.TaskSamplePlayer/'d__0')
    .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 2
    .locals init (
      [0] class TaskSample.TaskSamplePlayer/'d__0' V_0,
      [1] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder V_1
    )

    IL_0000: newobj       instance void TaskSample.TaskSamplePlayer/'d__0'::.ctor()
    IL_0005: stloc.0      // V_0
    IL_0006: ldloc.0      // V_0
    IL_0007: ldarg.0      // this
    IL_0008: stfld        class TaskSample.TaskSamplePlayer TaskSample.TaskSamplePlayer/'d__0'::'<>4__this'
    IL_000d: ldloc.0      // V_0
    IL_000e: call         valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
    IL_0013: stfld        valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
    IL_0018: ldloc.0      // V_0
    IL_0019: ldc.i4.m1
    IL_001a: stfld        int32 TaskSample.TaskSamplePlayer/'d__0'::'<>1__state'
    IL_001f: ldloc.0      // V_0
    IL_0020: ldfld        valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
    IL_0025: stloc.1      // V_1
    IL_0026: ldloca.s     V_1
    IL_0028: ldloca.s     V_0
    IL_002a: call         instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Startd__0'>(!!0/*class TaskSample.TaskSamplePlayer/'d__0'*/&)
    IL_002f: ldloc.0      // V_0
    IL_0030: ldflda       valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder TaskSample.TaskSamplePlayer/'d__0'::'<>t__builder'
    IL_0035: call         instance class [System.Runtime]System.Threading.Tasks.Task [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
    IL_003a: ret

  } // end of method TaskSamplePlayer::PlayAsync
~省略~
} // end of class TaskSample.TaskSamplePlayer

Play()

// Type: TaskSample.TaskSamplePlayer 
// Assembly: TaskSample, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: XXXX
// Location: C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.dll
// Sequence point data from C:\Users\example\OneDrive\Documents\workspace\TaskSample\TaskSample\bin\Debug\netcoreapp2.2\TaskSample.pdb

.class public auto ansi beforefieldinit
  TaskSample.TaskSamplePlayer
    extends [System.Runtime]System.Object
{
  .class nested private sealed auto ansi serializable beforefieldinit
    '<>c'
      extends [System.Runtime]System.Object
  {
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public static initonly class TaskSample.TaskSamplePlayer/'<>c' '<>9'

    .field public static class [System.Runtime]System.Action '<>9__0_0'

    .field public static class [System.Runtime]System.Action '<>9__1_0'

~省略~

    .method assembly hidebysig instance void
      'b__1_0'() cil managed
    {
      .maxstack 8

      // [15 35 - 15 36]
      IL_0000: nop

      // [15 37 - 16 26]
      IL_0001: ldstr        "hello"
      IL_0006: call         void [System.Console]System.Console::WriteLine(string)
      IL_000b: nop

      // [16 26 - 16 27]
      IL_000c: ret

    } // end of method '<>c'::'b__1_0'
  } // end of class '<>c'

~省略~

  .method public hidebysig instance class [System.Runtime]System.Threading.Tasks.Task
    Play() cil managed
  {
    .maxstack 2
    .locals init (
      [0] class [System.Runtime]System.Threading.Tasks.Task V_0
    )

    // [14 9 - 14 10]
    IL_0000: nop

    // [15 13 - 16 29]
    IL_0001: ldsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__1_0'
    IL_0006: dup
    IL_0007: brtrue.s     IL_0020
    IL_0009: pop
    IL_000a: ldsfld       class TaskSample.TaskSamplePlayer/'<>c' TaskSample.TaskSamplePlayer/'<>c'::'<>9'
    IL_000f: ldftn        instance void TaskSample.TaskSamplePlayer/'<>c'::'b__1_0'()
    IL_0015: newobj       instance void [System.Runtime]System.Action::.ctor(object, native int)
    IL_001a: dup
    IL_001b: stsfld       class [System.Runtime]System.Action TaskSample.TaskSamplePlayer/'<>c'::'<>9__1_0'
    IL_0020: call         class [System.Runtime]System.Threading.Tasks.Task [System.Runtime]System.Threading.Tasks.Task::Run(class [System.Runtime]System.Action)
    IL_0025: stloc.0      // V_0
    IL_0026: br.s         IL_0028

    // [17 9 - 17 10]
    IL_0028: ldloc.0      // V_0
    IL_0029: ret

  } // end of method TaskSamplePlayer::Play
~省略~
} // end of class TaskSample.TaskSamplePlayer

PlayAsync() の圧倒的長さ。。。

特に気になるのはこの辺り。

  • System.Runtime.CompilerServices.TaskAwaiter
  • System.Runtime.CompilerServices.IAsyncStateMachine
  • System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  • try - catch

上記のキーワードでググると有益な情報がどかどか出てくるので、これ僕が何か書く必要ないやん。。。てなります。
(ありがとうございます)

なお私も ILSpy を使ってみようかと思ったのですが、オプションから async メソッドをデコンパイルしない、というオプションが見つからなかったため断念しました。
GitHub の Isssue を見てると C# の複数バージョンに対応するため?に省かれたっポイですが。

なので細かいところはそちらを参照、というところなのですが、乱雑にまとめますと

  1. await を使うと IAsyncStateMachine を継承した構造体が作られる
    (リンク先を見ると StateMachine という名前らしいです)
  2. 非同期メソッドが完了したかなどの状態は TaskAwaiter が持つ
  3. 1.が AsyncTaskMethodBuilder を使った非同期メソッドの生成や 2.の登録・監視などを行う

といった感じでしょうか。

TaskAwaiter はコードを見ると interface を継承していますが、中身は空のようでした。

また、 await で処理を待ち受けることができる型は、 Awaitable パターンに沿っていること、という制限があるようです。

この Awaitable パターンというのは特定の型を指すのではなく、 GetAwaiter() など処理に必要なメソッドが実装されていれば良い、ということのようです。

動的な型を持つ言語や Go の interface もこんな感じでしたね。

この辺りで発生するエラーをキャッチするために try - catch があるのかもしれませんね。

IL のコードを載せたために長くなってしまったので、ここで切ります。

参照