【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::AwaitUnsafeOnCompleted d__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::Start d__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# の複数バージョンに対応するため?に省かれたっポイですが。
- 第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装 (1/2):連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回) - @IT
- 非同期メソッドの内部実装 - C# によるプログラミング入門 - ++C++; // 未確認飛行 C
- 非同期メソッド入門 (7) - 内部実装を覗く - xin9le.net
なので細かいところはそちらを参照、というところなのですが、乱雑にまとめますと
- await を使うと IAsyncStateMachine を継承した構造体が作られる
(リンク先を見ると StateMachine という名前らしいです) - 非同期メソッドが完了したかなどの状態は TaskAwaiter が持つ
- 1.が AsyncTaskMethodBuilder を使った非同期メソッドの生成や 2.の登録・監視などを行う
といった感じでしょうか。
TaskAwaiter はコードを見ると interface を継承していますが、中身は空のようでした。
また、 await で処理を待ち受けることができる型は、 Awaitable パターンに沿っていること、という制限があるようです。
この Awaitable パターンというのは特定の型を指すのではなく、 GetAwaiter() など処理に必要なメソッドが実装されていれば良い、ということのようです。
動的な型を持つ言語や Go の interface もこんな感じでしたね。
この辺りで発生するエラーをキャッチするために try - catch があるのかもしれませんね。
IL のコードを載せたために長くなってしまったので、ここで切ります。
参照
- dotnet/coreclr - GitHub
- スラスラわかる C#
- C# 7 Series, Part 2: Async Main – Mark Zhou's Tech Blog
- 非同期メソッドの内部実装 - C# によるプログラミング入門 - ++C++; // 未確認飛行 C
- 非同期メソッド入門 (7) - 内部実装を覗く - xin9le.net
- await可能なクラスを作ってみよう - かずきのBlog@hatena
- 第3回 非同期メソッドの内部実装とAwaitableパターンの独自実装 (1/2):連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門(最終回) - @IT