過去少し記事で触れた程度のasync/awaitですが、実は卒論の研究対象にするぐらいには大学時代からC#の沼に浸かっていたのですが、たまには内部展開をじっくり観察しとこうかなということで書いておきます。
↓いちおう過去に触れた記事
【C#】async/awaitの挙動をTask-Likeとawaitableパターンで変えてみる
卒論でそういうことをやったので書いておきます。(卒論で作ったプログラム)
blog.meilcli.net
【C#】IObservable<T>の最初のイベントだけをawaitで待機する話
唐突ですが、最近やっと仕事でもXamarin.Androidを使い始めました。
blog.meilcli.net
環境
大雑把に言うと現時点で見える最新のコードという感じですかね
コード
sharplabでの表示↓
SharpLab
C#/VB/F# compiler playground.
sharplab.io
この記事書くときに気づきましたが、sharplabってgistのコードを参照できるんですね
https://sharplab.io/#gist:4b64119d1dfe42277464d3266b4e4b15
gist id?を#gist:
で繋げたらgistのコードを参照してくれるようになるみたい
sharplabでのコンパイル結果:
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
using System.Threading.Tasks;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: UnverifiableCode]
public class Program
{
[Serializable]
[CompilerGenerated]
private sealed class <>c
{
public static readonly <>c <>9 = new <>c();
public static Func<int> <>9__0_0;
internal int <MethodAsync>b__0_0()
{
return 2;
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <MethodAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
private TaskAwaiter <>u__1;
private TaskAwaiter<int> <>u__2;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter2;
TaskAwaiter<int> awaiter;
switch (num)
{
default:
Console.WriteLine(0);
awaiter2 = Task.Delay(1000).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter2;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
return;
}
goto IL_006f;
case 0:
awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
goto IL_006f;
case 1:
{
awaiter = <>u__2;
<>u__2 = default(TaskAwaiter<int>);
num = (<>1__state = -1);
break;
}
IL_006f:
awaiter2.GetResult();
Console.WriteLine(1);
awaiter = Task.Run(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = <>c.<>9.<MethodAsync>b__0_0)).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 1);
<>u__2 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
break;
}
Console.WriteLine(awaiter.GetResult());
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<MethodAsync>d__0))]
public Task MethodAsync()
{
<MethodAsync>d__0 stateMachine = default(<MethodAsync>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
}
じっくり見ていこう
MethodAsync
がコンパイラーによって展開されると<>c
と<MethodAsync>d__0
という型が生成されていることがわかります。
<>c
はTask.Run(() => 2)
のラムダが展開されたクラスで今回の本題からは外れます。<MethodAsync>d__0
のほうが本題の展開された状態機械の構造体となります。
展開後のMethodAsync
の最初の数行は状態機械の準備部分となります。
<MethodAsync>d__0 stateMachine = default(<MethodAsync>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
AsyncTaskMethodBuilder <>t__builder = stateMachine.<>t__builder;
ここの部分ですね。ちなみにAsyncTaskMethodBuilder.Create()
は単にdefault(AsyncTaskMethodBuilder)
を返すだけのようです: ソース
<>t__builder.Start(ref stateMachine);
この行によって状態機械が開始されるので見ていきましょう。
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine =>
AsyncMethodBuilderCore.Start(ref stateMachine);
このようにAsyncTaskMethodBuilder.Start()
ではAsyncMethodBuilderCore.Start
を叩いてるだけです: ソース
さがすと同じファイルにあって、スレッド管理などを除けば単に以下のコードが核心部分です: ソース
try
{
stateMachine.MoveNext();
}
finally
// 以下いろいろ
つまり、引数のstateMachine.MoveNext
を呼び出していることになります。
そこで、引数となっているコンパイラー生成の状態機械のMoveNextを見に行くことにします。(ここで、状態機械初期化時に<>1__state = -1
がされてることを留意)
MoveNextは長いので切り離してみていきましょう:
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter2;
TaskAwaiter<int> awaiter;
switch (num)
{
<>1__state
によって状態分岐が行われます。switch(num)
では現在の状態の-1
に該当する分岐がないため、defalt分岐に行きます。
default:
Console.WriteLine(0);
awaiter2 = Task.Delay(1000).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter2;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
return;
}
goto IL_006f;
ここで、await Task.Delay(1000)
までの処理が現れました。Task.Delay
をawaitしているため、展開後はGetAwaiter()
で待機処理が挟まれています。いちおう解説しておくと、if(!awaiter2.IsCompleted)
の時点で処理が完了していたら、待機処理せずに終了処理のほうへgoto IL_006f;
でジャンプします。
もちろんのことながら、処理は完了しているわけもなく、待機処理へ突入します。(ここで状態が0
に更新されます)
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);
でAsyncTaskMethodBuilder.AwaitUnsafeOnCompleted
をTask.DelayのAwaiterとthisが引数として呼び出されています。
AsyncTaskMethodBuilder.AwaitUnsafeOnCompleted
ではAsyncTaskMethodBuilder<VoidTaskResult>.AwaitUnsafeOnCompleted
に処理を委譲しています: ソース
AsyncTaskMethodBuilder<VoidTaskResult>.AwaitUnsafeOnCompletedでは、いろいろと最適化処理があるようですが、関係ある部分だけを抜き出すとこのような感じです:
IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine);
try
{
awaiter.UnsafeOnCompleted(box.MoveNextAction);
}
GetStateMachineBox
のソースはこちらですが、内容をざっぱりみると、ExecutionContextの指定通りのスレッドで状態機械のMoveNextを実行するメソッドを提供しているようです。(ここは要検証かも)
そのためbox.MoveNextAction
でスレッドをまたいだMoveNextのAction
型を意味していて、それをawaiter.UnsafeOnCompleted
に渡しています。
awaiter
はTaskAwaiter
型なのでソースを見に行くと、UnsafeOnCompleted
は内部でOnCompletedInternal
を呼び出していて、そこではTask.SetContinuationForAwait
を呼び出しています: ソース
これ以上はソースが深すぎて読み切れないので、Taskの処理が終了後にTask.SetContinuationForAwait
で渡したbox.MoveNextAction
が呼び出されるのだろうという仮定のもと話を進めます。(メソッド名的にもさすがにそれ以外のことはしてないと思うし、思いたい)
そこで、状態が0
にされていたことを思い出しながら、MoveNext
メソッドに目を戻してやりましょう。
case 0:
awaiter2 = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
goto IL_006f;
次の分岐はこのようになりますが、状態は-1
に更新されつつもIL_006f
にジャンプします。
case 1:
{
awaiter = <>u__2;
<>u__2 = default(TaskAwaiter<int>);
num = (<>1__state = -1);
break;
}
IL_006f:
awaiter2.GetResult();
Console.WriteLine(1);
awaiter = Task.Run(<>c.<>9__0_0 ?? (<>c.<>9__0_0 = <>c.<>9.<MethodAsync>b__0_0)).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 1);
<>u__2 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
break;
IL_006f
の分岐のちょっと前から見ると状態が1
の分岐であることがわかります。
やっとawait Task.Delay(1000)
の後のConsole.WriteLine(1)
が見えてきました。そのあとのawait Task.Run(() => 2)
も同じような記述になっており、if(!awaiter.IsCompleted)
のとこまでに処理が完了してなかったら、状態を1
にして、また同じようなコードの深みに潜っていきます。
処理が完了していたらbreak
でこのswitchを抜けますし、完了していなくても次のMoveNext
でcase 1
にたどり着いて(たぶん)、状態を-1
に更新してbreak
されます。
やっと終わりが見えてきた気がする。
Console.WriteLine(awaiter.GetResult());
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
switchを抜けたら最後のConsole.WriteLine(result)
の部分が現れます。てか、ここtry文の中だったんですね、catch(Exception exception)
で思い出し()
何はともあれ、例外があればAsyncTaskMethodBuilder.SetException
が呼び出され、無事終了したらAsyncTaskMethodBuilder.SetResult
が呼び出されるところまで来ました。
SetResultとSetExceptionともに、AsyncTaskMethodBuilder<VoidTaskResult>
を呼び出しています。
さすがに力尽きたので、説明を省くと、SetResult
ではResultと一致するキャッシュされたTaskを返すか、新しくResultの値が設定されたTaskを生成して返しています。SetException
では文字通りTaskに対してExceptionを設定しています。
所感
ざっくりasync/awaitを見てきましたが、見たものは返り値がTask
なasyncメソッドで生成されたものです。C# 7.0でTask-Likeな型ならasyncメソッドの返り値にできるようになりました。Task-Likeでも同様な動作になりますが、部分的にはTask-Like実装者の好きなように動作を組み立てることができます。
まぁそれが大学の卒論の研究対象になったんだけどね。
GitHub - MeilCli/DistributedTask
Contribute to MeilCli/DistributedTask development by creating an account on GitHub.
github.com
Task-Like実装するならAsyncTaskMethodBuilderを再利用したほうが使い勝手がいい雰囲気は感じられただろうと思います、またAwaitableパターンもこのあたりのコンパイラーによる展開後のコードがわかれば実装しやすいと思います。
言い忘れてましたが、コンパイラーのバージョンとかでこのあたりの展開結果は変わると思いますのであしからず
あとIValueTaskSource
とかいろいろと最適化のコードが追加されているようですが、そっちのあたりはまだまだ追えてません………