F# RFC FS-1087 - 再開可能コード&状態機械をざっくり眺める(備忘録)
これはなに?
F# RFC FS-1087 - Resumable code and resumable state machinesを眺めたのをまとめた備忘録です.
注意
- 私の理解(この文書)が間違っている可能性がある.
- カテゴリはIdeaです.この文書のクオリティは低いです.読み飛ばして,ところどころ想像が入っているかもしれません.
こまごまとしたことは原文参照です.
(これから,大事なところだけ書いていきます.)
RFCのサマリ
静的に生成されたResumable code(State machine object)のための低レベルで一般的な能力をF#コンパイラに追加する.これによって,task
やtaskSeq
を含むいくつかのF#のコンピュテーション式がより効率の良い形で実装されるようになる.F#コンパイラに最初に実装されたシーケンス式の実装に似ている.
モチベーション
F#はとても一般化されたコンピュテーションを扱うためのメカニズムであるコンピュテーション式を持つ.(例: seq { ... }
, task { ... }
, async { ... }
, asyncSeq { ... }
, option { ... }
, etc)
task { ... }
のようなコンピュテーション式は,少ないアロケーションで実行されることが要求される.(他にも,シーケンス式と非同期シーケンス式など)既存のTaskBuilder.fsや,Plyといったものはアロケーションのオーバーヘッドが存在する.
F#のコンピュテーション式の,様々なコンピュテーションを表現できるという良さを残しつつ,ハイパフォーマンスなコードを生成する術を手に入れたい.
設計の方針
- 新しいシンタックスは言語仕様に追加しない.
- 静的に合成可能なResumable codeのみに焦点を当てる.Resumable codeのすべては一つのResumable state machineに結合される.
- F#のメタデータは変わらないようにする.(
nameof
,あるいはほかのF#の機能) - これをコンパイラの機能として扱う.
- コンパイル時に解決される.(リフレクションではない)
- この機能はコンパイルの遅い段階に完全に型検査される.(最初の型検査では完全に検査されない.)
- 低アロケーションなコンピュテーション式ビルダの実装は,高度なF#er向けである.
- コンピュテーション式に意味的な変更はない.
Resumable codeとは
Resumable codeは新しい低レベルでプリミティブ,合成可能でre-entrantなコードで,ハイパフォーマンスなコンピュテーション式の実装でのみ使われる.
type ResumableCode<'Data, 'T> = delegate of byref<ResumableStateMachine<'Data>> -> bool
Resumable codeは次のいずれか
- Resumable codeのコンビネータ(ResumableCode.Return, ResumableCode.Delay, ResumableCode.Combine など,あるいは
FSharp.Core
にあるほかの関数) - 新しく書かれた低レベルな
ResumableCode<_,_>(fun sm -> <optional-resumable-expr>)
デリゲートの実装.
Resumable codeをきっちり定義する理由は,MoveNext
メソッドを実装するためである.
Resumable codeのコンビネータ
ほとんどのResumable codeの関数はResumableCode.*
であり,Resumable codeとなる関数はすべてインライン化される.例:
let inline printThenYield () =
ResumableCode.Combine(
ResumableCode.Delay(fun () -> printfn "hello"; ResumableCode.Zero()),
ResumableCode.Yield()
)
ResumableCode.Yield
再開地点は``ResumableCode.Yield`の呼び出しで作られる:
ResumableCode.Yield()
すなわち,低レベルなResumable codeの中では:
let __stack_yield_complete = ResumableCode.Yield().Invoke(&sm)
この__stack_yield_complete
は(一時停止すればfalse
,再開すればtrue
を返す.
ResumableCode.Yield
は次の定義を持つ.
let inline Yield () : ResumableCode<'Data, unit> =
ResumableCode<'Data, unit>(fun sm ->
if __useResumableCode then
match __resumableEntry() with
| Some contID ->
sm.ResumptionPoint <- contID
false
| None ->
true
else
YieldDynamic(&sm))
ResumableCode.Combine
Resumable codeの連続実行を示す.
ResumableCode.Combine(<resumable-code>, <resumable-code>)
再開可能なコードを持つことから,<resumable-code>
は0か1つ以上の再開可能地点を持つ.これは,最初の<resumable-code>
が次の<resumable-code>
よりも実行されることが保証されないことを意味する.
ResumableCode.TryWith
try/with セマンティクス
ResumableCode.TryWith(<resumable-code>, <resumable-code>)
ResumableCode.TryFinally, ResumableCode.TryFinallyAsync
try/finally セマンティクス
ResumableCode.TryFinally(<resumable-code>, <compensation>)
ResumableCode.TryFinallyAsync(<resumable-code>, <resumable-code>)
これらコンビネータは.NET ILのtry
/finally
ブロックを用いず,try
/with
を用いる.
ResumableCode.While
反復のセマンティクス
ResumableCode.While((fun () -> expr), <resumable-code>)
再開可能なコードを持つことから,<resumable-code>
は0か1つ以上の再開可能地点を持つ.
しかし,ガードは再開可能ではない.非同期なwhileループ,非同期なwhileの条件が非同期であるループは,ガードのためのResumable codeを別な場所に配置する.
低レベルなResumable codeとは
低レベルなResumable codeはResumableCode
,__stateMachine
,MoveNextMethodImpl
というデリゲートの実装を持つ.ResumableCode
の戻り値は完了(true
)またはyielded(false
)を示す.
ResumableCode<_,_>(fun sm -> <optional-resumable-expr>)
MoveNextMethodImpl(fun sm -> <resumable-expr>)
例えば,
ResumableCode<_,_>(fun sm -> printfn "hello"; true)
<optional-resumable-expr>
は:
if __useResumableCode then <resumable-expr> else <expr>
または
<resumable-expr>
である.もし,Resumable codeがコンパイル可能である状態機械は<resumable-expr>
が用いられ,そうでない場合は<expr>
が用いられる.
<resumable-expr>
<resumable-expr>
は:
__resumableEntry
で作る再開可能ポイント
match __resumableEntry() with
| Some contId -> <resumable-expr>
| None -> <resumable-expr>
式が実行されると,最初のSome
の分岐が実行される.__resumeAt
を用いて再開が実行されると,None
分岐が実行される.
Some
分岐は普通は,__resumeAt
の実行をメソッドの実行時に行うと,のちの利用のためのcontID
を状態機械に保存することで一時停止する.
例として:
let inline returnFrom (task: Task<'T>) =
let mutable awaiter = task.GetAwaiter()
match __resumableEntry() with
| Some contID ->
sm.ResumptionPoint <- contID
sm.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm)
false
| None ->
sm.Result <- awaiter.GetResult()
true
注意:再開可能な式は結果を返すことができる.上の例では,taskが終了したかどうかを返している.
__resumeAt
式
__resumeAt <expr>
実行時,__resumeAt contId
はNone
分岐(対応するmatch
式の)に直接ジャンプする.
スコープにあるすべての__stack_*
のローカル変数は再開時にゼロ初期化される.
let
式
スタックに保存され,再開時にゼロ初期化されるlet __stack_var = ... in <resumable-expr>
Resumable codeの中では,__stack_*
という名前はスタックに保存され,再開時に初期値になる.
再開可能なtry/finally式
try <resumable-expr> finally <expr>
F#での注意:.NET ILはtry/withのコードブロックへの直接のジャンプは禁止されている.そのため,F#ではジャンプは再配置される.
再開可能なwhileループ
while <expr> do <resumable-expr>
ガードは再開可能な式ではない.
Resumable codeの並び
<resumable-stmt>; <resumable-stmt>
ResumableCode
デリゲートの呼び出し
e.g.
code arg
code.Invoke(&sm)
(code arg).Invoke(&sm)
match
式
再開可能なmatch <expr> with
| ... -> <resumable-expr>
| ... -> <resumable-expr>
そのほかのF#式
<expr>
Resumable codeを持つ再開可能な状態機械の構造体
再開可能な状態機械は__stateMachine
によって区別される.Resumable codeはコンパイラによって生成されるResumableStateMachine
を基にした構造体にホストされる.
__stateMachine<_, _>
(MoveNextMethod(fun sm -> <resumable-code>))
(SetMachineStateMethod(fun sm state -> ...))
(AfterMethod(fun sm -> ...))
コンパイル時,__stateMachine
はResumableStateMachine
にしたがって,F#コンパイラによってクロージャにキャプチャされたフィールドが追加された新しい構造体が生成される.MoveNextMethod
,SetMachineStateMethod
,AfterMethod
はこの構造体に書き換えされる.'関数'はIAsymcStateMachine
インターフェースを実装するのにつかわれる.AfterMethod
メソッドは実行され,ヒープ利用を削減するための状態機械の削除に使われる.それの返す型はResumableStateMachine
には含まれない.
[<Struct; NoComparison; NoEquality>]
type ResumableStateMachine<'Data> =
val mutable Data: 'Data
val mutable ResumptionPoint: int
val mutable ResumptionDynamicInfo: ResumptionDynamicInfo<'Data>
interface IResumableStateMachine<'Data>
interface IAsyncStateMachine
注意:
- 3つのデリゲートの引数は
MoveNext
,SetMachineState
メソッドの実装を特定する.また,After
コードブロックは状態機械の上で生成後すぐに実行される.デリゲートは状態機械のアドレスを受け取ることができる. - 生成するたびに,
ResumableStateMachine
構造体は新しい構造体にコピーされる.つまり,Resumableなコンピュテーション式を使うたびに構造体が定義されるということ? -
MoveNext
メソッドはResumable codeである可能性がある.
状態機械のコンパイル可能性
ステートマシンがコンパイル可能でないとは,次の条件のいずれかを満たすことを言う.
- Resumable codeが
__resumableEntry
の場所をボディとする整数によるfor
ループにならない. - Resumable codeが
let rec
になる. - Resumable codeがunreducedな
ResumableCode
のパラメータを利用している. - Resumable codeが
__resumableEntry
の地点でtry/finallyが用いられている. - Resumable codeがtry/withで
with
ブロックに__resumableEntry
がある.
状態機械の実行
状態機械の実行では,いくつかの項は直接.NETの処理に変換される.たとえば,__resumeAt
からgoto
など.
もしResumableCode
式が正しいResumable codeである場合,次のように変換される.
1. すべての実装は__useResumableCode
がtrue
の時,インライン化される.
2. すべての再開地点match __resumableEntry() with Some contId -> <stmt1> | None -> <stmt2>
はユニークな整数contID
のための静的な構造に変換される.<stmt1>
は最初に実行される.<stmt2>
はcontID
に対応するジャンプが起きたときに実行される.(ジャンプテーブルのターゲットとなる.)
3. すべての__stack_*
はメソッドのローカル変数に変換され,ゼロ初期化される.
4. すべての__stack_*
でない変数はオブジェクトのメンバ変数となる.
5. __resumeAt <expr>
はジャンプテーブルを呼ぶ.もし,静的に<expr>
が決まるならば,効率の良いgoto
文に置き換わる. そうでない場合,関数の最初に戻る.<expr>
が実行時に正しい再開地点を指定できない場合,__resumeAt
の後の文を実行する.
task{...}
の実装を見てみよう
正式リリース後:task{...}
の実装を見てみよう.まずはC#の結果を参考にしてみる.
簡単な例
コンパイルしてみる
C#にて,
public static async ValueTask<int> Fuga() {
await Task.Delay(1000);
return 0;
}
このプログラムをコンパイルしてILSpyでC#4.0として見てみた.(少し整形した)
private sealed class <Fuga>d__0 : IAsyncStateMachine {
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
private TaskAwaiter <>u__1;
private void MoveNext() {
int num = <>1__state;
int result;
TaskAwaiter awaiter;
if (num != 0) {
awaiter = Task.Delay(1000).GetAwaiter();
if (!awaiter.IsCompleted) {
num = (<>1__state = 0);
<>u__1 = awaiter;
<Fuga>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
} else {
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
result = 0;
<>1__state = -2;
<>t__builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext() => this.MoveNext();
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) =>
this.SetStateMachine(stateMachine);
}
public static Task<int> Fuga() {
<Fuga>d__0 stateMachine = new <Fuga>d__0();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
今度はF#のコードである.
let fuga () =
task {
do! Task.Delay(1000)
return 0
}
↓
[SpecialName]
[StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
internal struct fuga@17 : IAsyncStateMachine, IResumableStateMachine<TaskStateMachineData<int>>
{
public TaskStateMachineData<int> Data;
public int ResumptionPoint;
public TaskAwaiter awaiter;
public override void MoveNext() {
int resumptionPoint = ResumptionPoint;
switch (resumptionPoint){}
bool flag;
int num;
bool flag2;
switch (resumptionPoint) {
default: {
Task task = Task.Delay(1000);
awaiter = task.GetAwaiter();
flag = true;
if (awaiter.IsCompleted)
break;
if (false)
goto case 1;
ResumptionPoint = 1;
num = 0;
goto IL_0064;
}
case 1:
num = 1;
goto IL_0064;
IL_0064:
flag2 = (byte)num != 0;
flag = flag2;
break;
}
int num2;
if (flag) {
awaiter.GetResult();
@null @null = null;
@null null2 = null;
int result = 0;
Data.Result = result;
num2 = 1;
} else {
Data.MethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
num2 = 0;
}
int num3;
if (num2 != 0) {
TaskAwaiter taskAwaiter = default(TaskAwaiter);
awaiter = taskAwaiter;
num3 = 1;
} else
num3 = 0;
if (num3 != 0)
Data.MethodBuilder.SetResult(Data.Result);
}
void IAsyncStateMachine.MoveNext() => this.MoveNext();
public override void SetStateMachine(IAsyncStateMachine state) =>
Data.MethodBuilder.SetStateMachine(state);
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine state) => this.SetStateMachine(state);
public override int get_ResumptionPoint() => ResumptionPoint;
int IResumableStateMachine<TaskStateMachineData<int>>.get_ResumptionPoint() =>
this.get_ResumptionPoint();
public override TaskStateMachineData<int> get_Data() => Data;
TaskStateMachineData<int> IResumableStateMachine<TaskStateMachineData<int>>.get_Data() =>
this.get_Data();
public override void set_Data(TaskStateMachineData<int> value) => Data = value;
void IResumableStateMachine<TaskStateMachineData<int>>.set_Data(TaskStateMachineData<int> value) => this.set_Data(value);
}
public static Task<int> fuga() {
fuga@17 stateMachine = default(fuga@17);
stateMachine.Data.MethodBuilder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.Data.MethodBuilder.Start(ref stateMachine);
return stateMachine.Data.MethodBuilder.Task;
}
Microsoft.FSharp.Control.TaskStateMachineData<T>
とは次のように定義されている.
[<Struct; NoComparison; NoEquality>]
type TaskStateMachineData<'T> =
[<DefaultValue(false)>]
val mutable Result: 'T
[<DefaultValue(false)>]
val mutable MethodBuilder: AsyncTaskMethodBuilder<'T>
and TaskStateMachine<'TOverall> = ResumableStateMachine<TaskStateMachineData<'TOverall>>
and TaskResumptionFunc<'TOverall> = ResumptionFunc<TaskStateMachineData<'TOverall>>
and TaskResumptionDynamicInfo<'TOverall> = ResumptionDynamicInfo<TaskStateMachineData<'TOverall>>
and TaskCode<'TOverall, 'T> = ResumableCode<TaskStateMachineData<'TOverall>, 'T>
つまり,AsyncTaskMethodBuilder
と結果の一時保存場所である.
TaskBuilder.Run
を見てみる
まずはRun
メソッドである.
member inline _.Run(code: TaskCode<'T, 'T>) : Task<'T> =
if __useResumableCode then
__stateMachine<TaskStateMachineData<'T>, Task<'T>>
(MoveNextMethodImpl<_>(fun sm ->
//-- RESUMABLE CODE START
__resumeAt sm.ResumptionPoint
let __stack_code_fin = code.Invoke(&sm)
if __stack_code_fin then
sm.Data.MethodBuilder.SetResult(sm.Data.Result)
//-- RESUMABLE CODE END
))
(SetStateMachineMethodImpl<_>(fun sm state -> sm.Data.MethodBuilder.SetStateMachine(state)))
(AfterCode<_, _>(fun sm ->
sm.Data.MethodBuilder <- AsyncTaskMethodBuilder<'T>.Create ()
sm.Data.MethodBuilder.Start(&sm)
sm.Data.MethodBuilder.Task))
else
TaskBuilder.RunDynamic(code)
もし,Resumable codeが利用できるならば,__useResumableCode
がtrue
となり,__stateMachine
が処理される.
まず,AfterCode
は,関数fuga
に出力されていたコードがそのまま記述されている.
そして,本丸となるMoveNextMethodImpl
である.最初の__resumeAt
で処理を再開している.これは,出力されたコードの最初にあるswitch
文がそれにあたると考えられる.そして,code.Invoke(&sm)
を実行している.これはコード本文の実行を意味する.そのコード本文の実行が終われば,AsyncTaskMethodBuilder.SetResult
を呼び出して,Result
に格納された結果を返している.
ちなみに,DynamicRun
は,
static member RunDynamic(code: TaskCode<'T, 'T>) : Task<'T> =
let mutable sm = TaskStateMachine<'T>()
let initialResumptionFunc = TaskResumptionFunc<'T>(fun sm -> code.Invoke(&sm))
let resumptionInfo =
{ new TaskResumptionDynamicInfo<'T>(initialResumptionFunc) with
member info.MoveNext(sm) =
let mutable savedExn = null
sm.ResumptionDynamicInfo.ResumptionData <- null
let step = info.ResumptionFunc.Invoke(&sm)
if step then
sm.Data.MethodBuilder.SetResult(sm.Data.Result)
else
let mutable awaiter = sm.ResumptionDynamicInfo.ResumptionData :?> ICriticalNotifyCompletion
sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm)
member _.SetStateMachine(sm, state) =
sm.Data.MethodBuilder.SetStateMachine(state)
}
sm.ResumptionDynamicInfo <- resumptionInfo
sm.Data.MethodBuilder <- AsyncTaskMethodBuilder<'T>.Create ()
sm.Data.MethodBuilder.Start(&sm)
sm.Data.MethodBuilder.Task
となる.ここで,ResumptionDynamicInfo<'T>
が役に立つ.初期の再開後メソッドはコード本文として,ResumptionDynamicInfo
を作成している.MoveNext
が呼び出されると,ResumptionDynamicInfo.ResumptionData
を呼び出す.step
がtrue
,つまり,完了したならばAsyncTaskMethodBuilder.SetResult
を呼び出して結果を返す.false
ならば,ResumptionDynamicInfo.ResumptionData
を完了後に実行されるICriticalNotifyCompletion
として指定している.このメンバはobj
と型付けされている.(!)
Do
を見てみる
member inline _.Combine
(
task1: TaskCode<'TOverall, unit>,
task2: TaskCode<'TOverall, 'T>
) : TaskCode<'TOverall, 'T> =
ResumableCode.Combine(task1, task2)
こんなことになっているので,ResumableCode.Combine
を見てみる.
ResumableCodeは,
let __stack_fin = code1.Invoke(&sm)
if __stack_fin then
code2.Invoke(&sm)
else
false
かえってシンプルである.一方,CombineDynamic
は,
if code1.Invoke(&sm) then
code2.Invoke(&sm)
else
let rec resume (mf: ResumptionFunc<'Data>) = ResumptionFunc<'Data>(fun sm ->
if mf.Invoke(&sm) then
code2.Invoke(&sm)
else
sm.ResumptionDynamicInfo.ResumptionFunc <- (resume (GetResumptionFunc &sm))
false)
sm.ResumptionDynamicInfo.ResumptionFunc <- (resume (GetResumptionFunc &sm))
false
code1
が素直に終わる(Task.IsCompleted
がすぐにtrue
になる)ならば,code2
を実行している.一方で終わらなかった場合,再開後のメソッドをresume
を代入している.再開後のコードを実行してみて,終了すればcode2
を実行し,そうでなければ,同じようにresume
を実行する.そのままGetResumptionFunc &sm
を代入しないのは,そのまま終了すると,code2
が実行されずに結果を返してしまうためである.
Return
を見てみる
Returnは非常に簡単である.
member inline _.Return(value: 'T) : TaskCode<'T, 'T> =
TaskCode<'T, _>(fun sm ->
sm.Data.Result <- value
true)
Data.Result
に値を代入してtrue
を返す.そうすれば,AfterCode
でSetResult
を読んでくれるので,結果を返すことができる.
Bind
を見てみる.
let inline _. Bind (task : ^TaskLike, continuation: ('TResult1 -> TaskCode<'TOverall, 'TResult2>))
TaskCode<'TOverall, _>(fun sm ->
if __useResumableCode then
//-- RESUMABLE CODE START
// Get an awaiter from the awaitable
let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task))
let mutable __stack_fin = true
if not (^Awaiter: (member get_IsCompleted: unit -> bool) (awaiter)) then
// This will yield with __stack_yield_fin = false
// This will resume with __stack_yield_fin = true
let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm)
__stack_fin <- __stack_yield_fin
if __stack_fin then
let result = (^Awaiter: (member GetResult: unit -> 'TResult1) (awaiter))
(continuation result).Invoke(&sm)
else
sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm)
false
else
TaskBuilderBase.BindDynamic< ^TaskLike, 'TResult1, 'TResult2, ^Awaiter, 'TOverall>(
&sm,
task,
continuation
)
//-- RESUMABLE CODE END
)
もし,Task.IsCompleted
がすぐにtrue
にならなかったら(終わらなかったら),ResumableCode.Yield()
を用いている.
一方で,すぐに終わった場合は,Task.Awaiter.GetResult()
で値を取得し,継続を呼び出している.
詳しく,ResumableCode.Yield
を見てみよう.
//-- RESUMABLE CODE START
match __resumableEntry () with
| Some contID ->
sm.ResumptionPoint <- contID
//if verbose then printfn $"[{sm.Id}] Yield: returning false to indicate yield, contID = {contID}"
false
| None ->
//if verbose then printfn $"[{sm.Id}] Yield: returning true to indicate post-yield"
true
ここでは,再開点を挿入している.
つまり,1回目通る時はResumableCode.Yield.Invoke(&sm)
は,false
を返す.__stack_fin
はfalse
になるので,AwaitUnsafeOnCompleted
が呼ばれる.awaiter
の実行が終了すれば,MoveNext
が再度呼ばれ,Run
の最初の__resumeAt
が呼ばれる.すると,None
分岐にジャンプし,true
をResumableCode.Yield.Invoke(&sm)
が返すこととなる.__stack_fin
はtrue
になるので,GetResult
などが呼ばれることとなる.
BindDynamic
は次のとおりである.
let mutable awaiter = (^TaskLike: (member GetAwaiter: unit -> ^Awaiter) (task))
let cont =
(TaskResumptionFunc<'TOverall>(fun sm ->
let result = (^Awaiter: (member GetResult: unit -> 'TResult1) (awaiter))
(continuation result).Invoke(&sm)))
// shortcut to continue immediately
if (^Awaiter: (member get_IsCompleted: unit -> bool) (awaiter)) then
cont.Invoke(&sm)
else
sm.ResumptionDynamicInfo.ResumptionData <- (awaiter :> ICriticalNotifyCompletion)
sm.ResumptionDynamicInfo.ResumptionFunc <- cont
false
すぐに終了したならば,cont
が実行される.その一方で,終わらなかった場合,ResumptionData
にawaiter
,ResumptionFunc
にcont
が代入される.
RunDynamic
に戻ると,このawaiter
がAwaitUnsafeOnCompleted
に渡され,cont
が次回呼び出される.
task{...}
の実装を見てみよう
正式リリース前:task{...}
の実装を見てみたい.しかし,これだけ見ても理解は大変なので,C#/VBの結果を参考にしてみてみよう.
簡単な例
C#/VBでAsync/Awaitを使ったらどうなるのか
Module Program
Async Function Fuga() As Task(Of Integer)
Await Task.Delay(100)
Return 0
End Function
End Module
このプログラムをコンパイルしてILSpyでasync/awaitのデコンパイルを無効化したうえで見てみた.(ILSpyがC#でしか出力できないんだから,C#で書くべきだった…)
まず,Fugaが生成した状態機械のためのクラスとFuga()
は次の通りだ.(どうでもいい部分は消した)
private sealed class VB$StateMachine_0_Fuga : IAsyncStateMachine
{
public int $State;
public AsyncTaskMethodBuilder<int> $Builder;
internal TaskAwaiter $A0;
internal void MoveNext() { /* ... */ }
}
public static Task<int> Fuga()
{
VB$StateMachine_0_Fuga stateMachine = new VB$StateMachine_0_Fuga();
stateMachine.$State = -1;
stateMachine.$Builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.$Builder.Start(ref stateMachine);
return stateMachine.$Builder.Task;
}
$State
はコードのどの場所まで行ったかを記憶する.$builder
と$A0
は,Asyncのために必要なものである.
メソッドFuga()
は,状態機械を作り,その中の$Builder
を初期化して,Start(ref TStateMachine)
している.Start
では,$Builder.Task
を作ったのちに,VB$StateMachine_0_Fuga.MoveNext()
を呼び出している.
VB$StateMachine_0_Fuga.MoveNext()
は次の通りだ.(try-catchを消したのでインデントがおかしい)
internal void MoveNext()
{
int num = $State;
int result;
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = Task.Delay(100).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = ($State = 0);
$A0 = awaiter;
$Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else { /* 後始末コード */ }
awaiter.GetResult();
awaiter = default(TaskAwaiter);
result = 0;
num = ($State = -2);
$Builder.SetResult(result);
}
Fuga()
が呼ばれるとき,$State
は-1
から始まる.つまり,if分のthen節が実行される.そこでは,Await Task.Delay(100)
があるので,awaiter = Task.Delay(100).GetAwaiter();
で実行されていることが分かる.すぐ終わらなかった場合(awaiter.IsCompleted == false
)は,$State
を0にする(次MoveNext
が呼ばれたときに後始末するようにする).そして,$Builder.AwaitUnsafeOnCompleted
を呼ぶ.これは,awaiter
が完了したときに,this.MoveNext()
が呼ばれるようになる.そして,returnしている.一方で次にMoveNext
呼ばれた場合か,すぐに終わった場合,result
に0を設定後,$Builder.SetResult(result)
($Builder.Task.Result
の値を設定している.)とreturn分に書いた値を返しているのが分かる.
task!{ ... }
はどうか
task!{ ... }
の実装はここにある.
状態機械
[<Struct; NoComparison; NoEquality>]
type TaskStateMachineData<'TOverall> =
val mutable Result : 'TOverall
val mutable Awaiter: ICriticalNotifyCompletion
val mutable MethodBuilder : AsyncTaskMethodBuilder<'TOverall>
and TaskStateMachine<'TOverall> = ResumableStateMachine<TaskStateMachineData<'TOverall>>
and TaskResumptionFunc<'TOverall> = ResumptionFunc<TaskStateMachineData<'TOverall>>
and TaskResumptionDynamicInfo<'TOverall> = ResumptionDynamicInfo<TaskStateMachineData<'TOverall>>
and TaskCode<'TOverall, 'T> = ResumableCode<TaskStateMachineData<'TOverall>, 'T>
TaskStateMachineData
は状態機械のデータを持つ構造体で,先ほどの状態機械とメンバが非常に似ている.TaskStateMachine
は状態機械なのだろうが,ResumableStateMachine
のジェネリクスに適用したものとなっている.他もResumableほにゃらら
を使っている.
こいつらは,次のとおりである.
/// Acts as a template for struct state machines introduced by __stateMachine, and also as a reflective implementation
[<Struct; NoComparison; NoEquality>]
type ResumableStateMachine<'Data> =
val mutable Data: 'Data
val mutable ResumptionPoint: int
/// Represents the delegated runtime continuation of a resumable state machine created dynamically
val mutable ResumptionDynamicInfo: ResumptionDynamicInfo<'Data>
interface IResumableStateMachine<'Data> with
member sm.ResumptionPoint = sm.ResumptionPoint
member sm.Data with get() = sm.Data and set v = sm.Data <- v
interface IAsyncStateMachine with
// Used for dynamic execution. For "__stateMachine" it is replaced.
member sm.MoveNext() =
sm.ResumptionDynamicInfo.MoveNext(&sm)
// Used when dynamic execution. For "__stateMachine" it is replaced.
member sm.SetStateMachine(state) =
sm.ResumptionDynamicInfo.SetStateMachine(&sm, state)
and ResumptionFunc<'Data> = delegate of byref<ResumableStateMachine<'Data>> -> bool
and [<AbstractClass>]
ResumptionDynamicInfo<'Data>(initial: ResumptionFunc<'Data>) =
member val ResumptionFunc: ResumptionFunc<'Data> = initial with get, set
abstract MoveNext: machine: byref<ResumableStateMachine<'Data>> -> unit
abstract SetStateMachine: machine: byref<ResumableStateMachine<'Data>> * machineState: IAsyncStateMachine -> unit
type ResumableCode<'Data, 'T> = delegate of byref<ResumableStateMachine<'Data>> -> bool
これらは,コンピュテーション式を実装するうえで大変便利な型と関数のテンプレートになる.(すこし下の方に行くと,module ResumableCode
がある.そのモジュールにある関数らはTaskBuilder
のWhile
などの関数がそのまま呼び出している.)
ResumableStateMachine
は途中の値を持つData
,VB/C#の$State
にあたるResumptionPoint
を持つ.ResumableDynamicInfo
とはコンパイル時合成できなかった時のためのクラスである.
TaskBuilder.Run
実際に,TaskBuilder.Run
を見てみよう.(言語仕様は変わらないので,ビルダーももちろんある.)
type TaskBuilder() =
member inline _.Run(code : TaskCode<'T, 'T>) : Task<'T> =
if __useResumableCode then
__stateMachine<TaskStateMachineData<'T>, Task<'T>>
(MoveNextMethodImpl<_>(fun sm ->
if __useResumableCode then
//-- RESUMABLE CODE START
__resumeAt sm.ResumptionPoint
try
let __stack_code_fin = code.Invoke(&sm)
if __stack_code_fin then
sm.Data.MethodBuilder.SetResult(sm.Data.Result)
with exn ->
sm.Data.MethodBuilder.SetException exn
//-- RESUMABLE CODE END
else
failwith "unreachable"))
(SetStateMachineMethodImpl<_>(fun sm state -> sm.Data.MethodBuilder.SetStateMachine(state)))
(AfterCode<_,_>(fun sm ->
sm.Data.MethodBuilder <- AsyncTaskMethodBuilder<'T>.Create()
sm.Data.MethodBuilder.Start(&sm)
sm.Data.MethodBuilder.Task))
else
TaskBuilder.RunDynamic(code)
__useResumableCode
で分岐しているのが分かるだろう.then節が合成される状態機械のコードであろう.
まず,__stateMachine
関数にMoveNextMethodImpl
,SetStateMachineMethodImpl
,AfterCode
となる関数を渡す.これら4つは,全てコンパイラに教えるための関数,ディレクティブみたいなもので,実装を見にいっても,(ResumableCode
と同じところにある.)failwith
があるだけで,何もない.MoveNextMethodImpl
はMoveNext()
に相当する関数,SetStateMachineMethodImpl
,AfterCode
は初期化に使われる関数である.SetStateMachine
関数はAsyncTaskMethodBuilder
に状態機械を紐づける関数である.AfterCode
は先ほどのFuga()
関数と見比べると分かりやすいだろう.これから想像するに,task! { ... }
を書いた箇所でそのまま実行されるのだろう.
MoveNextMethodImpl
の中身を見てみる.
__resumeAt sm.ResumptionPoint
try
let __stack_code_fin = code.Invoke(&sm)
if __stack_code_fin then
sm.Data.MethodBuilder.SetResult(sm.Data.Result)
with exn ->
sm.Data.MethodBuilder.SetException exn
__resumeAt
は分からないので後回しにしよう.そのあと,__stack_code_fin
というlet定義があるが,__stack
が付くと,スタック上に確保される(状態機械の状態にはならない)変数となる.渡されたResumable codeを実行して,trueならば結果を格納する.Resumable codeの戻り値は終わったか終わってないかを表しているのが分かるだろうか.
TaskBuilder.RunDynamic
はすぐ上にあるが,TaskResumptionDynamicInfo
が活躍している.TaskResumptionDynamicInfo
は静的なコード生成が出来なかった時用であることがわかる.
TaskBuilder.ReturnFrom
次に,TaskBuilder.ReturnFrom
を見てみよう.
member inline _.ReturnFrom (task: Task<'T>) : TaskCode<'T, 'T> =
TaskCode<'T, _>(fun sm ->
if __useResumableCode then
//-- RESUMABLE CODE START
// This becomes a state machine variable
let mutable awaiter = task.GetAwaiter()
let mutable __stack_fin = true
if not task.IsCompleted then
// This will yield with __stack_yield_fin = false
// This will resume with __stack_yield_fin = true
let __stack_yield_fin = ResumableCode.Yield().Invoke(&sm)
__stack_fin <- __stack_yield_fin
if __stack_fin then
sm.Data.Result <- awaiter.GetResult()
true
else
sm.Data.MethodBuilder.AwaitUnsafeOnCompleted(&awaiter, &sm)
false
else
TaskBuilder.ReturnFromDynamic(&sm, task)
//-- RESUMABLE CODE END
)
awaiter
は状態機械のメンバ変数になる変数で,先頭に__stack_
が付いていない.そして,VB/C#と同様に,GetAwaiter
を呼んでいる.もし,実行するtask
がすぐに終わったならば,真ん中のif文には引っかからずに,__stack_fin
がtrue
となり,すぐにGetResult
されて,結果に代入される.一方で,終わらなかった場合,ResumableCode.Yield()
を呼び出し,その結果によって条件分岐をしている.RFCを見ると,ResumableCode.Yield
はコードを中断するときにfalse
を返し,コードが再開されるときにtrue
を返す.つまり,呼び出した時にfalse
を返す.この時,実行中のResumableCode
が終了して,また次に呼ばれるときに,ここから再開され,true
を返すようになる,ということだろうか.この関数は,次のように実装されている.
let inline Yield () : ResumableCode<'Data, unit> =
ResumableCode<'Data, unit>(fun sm ->
if __useResumableCode then
match __resumableEntry() with
| Some contID ->
sm.ResumptionPoint <- contID
false
| None ->
true
else
YieldDynamic(&sm))
RFCでは,match __resumableEntry() with
の部分を特別扱いするような記述がある.ここから再開されそうな雰囲気がある.前述した変数awaiter
は,記憶するようにしているので,それと合わせると納得がいく.
さて,このYield
のコードについて詳しく見ていこう.このmatch __resumableEntry() with
について,RFCに,
このような式が実行されるとき,最初は
Some
の分岐の方が実行される.どんなに再開地点が設定されていても,もし再開が__resumeAt
を用いて実行されるならば,None
の分岐の方が実行される.
初回実行時は,Some
の方を通り,ラベル番号を__resumableEntry()
で取得し,後々gotoするために,sm.ResumptionPoint
に代入して記憶する.今度MoveNext
が呼ばれたときに,__resumeAt
でgotoして,None
の方を通る,というわけであろう.
では,どこに__resumeAt
があるか,Run
に書かれたMoveNext
の実装を思い出してみると,
__resumeAt sm.ResumptionPoint
try
let __stack_code_fin = code.Invoke(&sm)
if __stack_code_fin then
sm.Data.MethodBuilder.SetResult(sm.Data.Result)
with exn ->
sm.Data.MethodBuilder.SetException exn
というわけで,match __resumableEntry with None ->
の地点でgotoのラベルが入ってそうで,__resumeAt
の地点でgotoしていそうである.(実際は後にcode
を呼び出しているので,ジャンプ先を指定して,呼び出した後にgotoしていそうである.)
結び
非常に簡単にアロケーションの少ないコード生成ができるなぁという印象でした.
驚くべきことが,C#/VBのステートマシンはclassなのに対し,F#はstructで生成されています.どうせボックス化されるので,微妙な気もしますが,F#の仕組みは汎用ということで,ハイパフォーマンスなコンピュテーション式を作ることができるようにということなのでしょう.
Discussion