Share
Embed
Transcript
93 slides
-
自己紹介 1 • 所属: NTTコミュニケーションズ イノベーションセンター • 趣味: C#, OSS,
ドール, 一眼(α7 IV) • 執心領域 • C# ⇔ TypeScript • SignalR 何縫ねの。 nenoNaninu nenoMake ブログ https://blog.neno.dev その他 https://neno.dev -
OSS 紹介 2 属性を付与するだけ Tapper • C# の型定義から TypeScript の型定義を生成する
.NET Tool/ library • JSON / MessagePack 対応! https://github.com/nenoNaninu/Tapper -
OSS 紹介 3 • C# の SignalR Client を強く型付けするための Source
Generator TypedSignalR.Client Before After (using TypedSignalR.Client) こんな SignalR の Hub と Receiver の interface が あったとして… 脱文字列! 全てが強く型付け! https://github.com/nenoNaninu/TypedSignalR.Client -
4 • TypeScript の SignalR Client を強く型付けするための .NET Tool /
library TypedSignalR.Client.TypeScript Before After (using TypedSignalR.Client.TypeScript) 脱文字列! 全てが強く型付け! TypeScript 用の型を C# から自動生成 MessagePack Hub Protocol 対応! https://github.com/nenoNaninu/TypedSignalR.Client.TypeScript 属性を付与するだけ! OSS 紹介 -
5 • SignalR 使ったアプリを快適に開発するための GUI を自動生成する library • 2 step
で利用可能! • http pipeline に middleware の追加 • Hub と Receiver を定義してる interface に属性を付与 • JWT 認証 サポート • パラメータのユーザ定義型サポート • JSON で入力! SignalR 版 SwaggerUI TypedSignalR.Client.DevTools https://github.com/nenoNaninu/TypedSignalR.Client.DevTools OSS 紹介 -
async/await 以前 8 • async/await が無かった時代は、ContinueWith メソッドを使って continuation を コールバックとして登録していた。
その当時の非同期はコールバック… どうすれば脱コールバック化を果たせるだろうか? どうすれば同期的な書き心地を得られるだろうか? -
C# のイテレータ 15 • 手動で MoveNext を叩くことで、任意 & 適切なタイミングで コルーチンを進ませること可能。
IEnumerable<T> 非同期処理が完了した際に呼ばれる continuation として MoveNext() を 呼び出せるとしたら、どうだろうか? -
C# のイテレータ 16 • こんなヘルパー関数を用意してあげる。 • IterateAsync • Process というローカル関数内で
MoveNext() を呼び出しイテレータを進め、 Current で取得した Task インスタンスに continuation を ContinueWith() 経由で登録 IEnumerable<Task> イテレータの Current で取得した Task が完了したら、引数で渡された イテレータの MoveNext() が呼び出される -
C# のイテレータ 17 • Task インスタンスを yield return するメソッドを定義 •
その返り値を IterateAsync に 渡してあげる。 IterateAsync の何が嬉しいのか? -
C# のイテレータ 18 • Task インスタンスを yield return するメソッドを定義 •
その返り値を IterateAsync に 渡してあげる。 IterateAsync の何が嬉しいのか? 脱コールバック!! -
C# のイテレータ 19 • Task インスタンスを yield return するメソッドを定義 •
その返り値を IterateAsync に 渡してあげる。 IterateAsync の何が嬉しいのか? 脱コールバック!! yield return を await に 置き換えて読んでみると、おや…? -
C# のイテレータ 20 • Task インスタンスを yield return するメソッドを定義 •
その返り値を IterateAsync に 渡してあげる。 IterateAsync の何が嬉しいのか? 脱コールバック!! yield return を await に 置き換えて読んでみると、おや…? C# コンパイラのイテレータと async/await をサポートするための ロジックの約95%は共有されている -
Compiler Transform 22 • async メソッドは右の様に展開される • 非 async メソッド
+ ステートマシン になる。 • ステートマシンに実装されている interface コンパイラの仕事 -
Compiler Transform 23 • ステートマシン (struct) • async メソッドに記述されたロジックを コンパイラがステートマシンに変換。
• ステートマシンには元々のロジックに加え 進行状態を保存するフィールドなども 生えている (イテレータとほぼ同じ!) • IAsyncStateMachine を実装 • AsyncTaskMethodBuilder (struct) • Task プロパティとして露出する Task 型のオブジェクトを作成 • それに対する結果の設定 • SetResult / SetException • await した完了してない Task に対する continuation の登録 • AwaitOnCompleted / AwaitUnsafeOnCompleted 大事な登場人物 -
Compiler Transform 24 • ステートマシン (struct) • async メソッドに記述されたロジックを コンパイラがステートマシンに変換。
• ステートマシンには元々のロジックに加え 進行状態を保存するフィールドなども 生えている (イテレータとほぼ同じ!) • IAsyncStateMachine を実装 • AsyncTaskMethodBuilder (struct) • Task プロパティとして露出する Task 型のオブジェクトを作成 • それに対する結果の設定 • SetResult / SetException • await した完了してない Task に対する continuation の登録 • AwaitOnCompleted / AwaitUnsafeOnCompleted 大事な登場人物 双方共に struct サスペンドがなければ zero allocation -
Compiler Transform 25 1. ステートマシン (struct)の初期化 • メソッドの引数や初期状態を格納 2. AsyncTaskMethodBuilder
(struct) の作成 • 内部的には default を返しているだけ。 • ステートマシンに格納される 3. AsyncTaskMethodBuilder.Start で ステートマシンのロジックを開始 4. Builder 経由で返り値である Task オブジェクトを取得し、return。 • stateMachine.<>t__builder.Task メソッドが呼ばれてからの処理内容 -
Compiler Transform 26 1. ステートマシン (struct)の初期化 • メソッドの引数や初期状態を格納 2. AsyncTaskMethodBuilder
(struct) の作成 • 内部的には default を返しているだけ。 • ステートマシンに格納される 3. AsyncTaskMethodBuilder.Start で ステートマシンのロジックを開始 4. Builder 経由で返り値である Task オブジェクトを取得し、return。 • stateMachine.<>t__builder.Task メソッドが呼ばれてからの処理内容 -
Compiler Transform 27 • AsyncTaskMethodBuilder.Start メソッドは何をやっているのか? • ステートマシンの操作という点では、MoveNext を叩いているだけ。 •
何故直接 ステートマシンの MoveNext メソッドを呼ばないのか? • Start メソッドでは、細かいけど大事な事をやっている。それはなにか? • ExecutionContext を理解する必要があります。 AsyncTaskMethodBuilder.Start(ref stateMachine) -
ExecutionContext 29 • 明示的なデータの渡し方 • メソッドからメソッドに引数でデータを渡すパターン。 • こちらは特に問題ない。 • 暗黙的なデータの渡し方
• Static フィールドなどにデータを格納し、どこかでそれを読み出すパターン。 • こういったデータはよく ambient data とか呼ばれる。 データの渡し方のパターン -
ExecutionContext 30 • 明示的なデータの渡し方 • メソッドからメソッドに引数でデータを渡すパターン。 • こちらは特に問題ない。 • 暗黙的なデータの渡し方
• Static フィールドなどにデータを格納し、どこかでそれを読み出すパターン。 • こういったデータはよく ambient data とか呼ばれる。 データの渡し方のパターン async メソッドの場合、 ambient data の渡し方で 様々な課題が発生する -
ExecutionContext 31 • 普通の static フィールドの利用 • async メソッドは通常、平行ないし並列で実行される。 •
ある async control flow で書き換えた static フィールドは、別の flow で書き換えられてしまう。 • 競合を避けるためには1つのメソッドしか実行することができない。 • つまり ambient data の渡し方として成立しない。 Ambient data の渡し方のパターンと async メソッドでの問題点 -
ExecutionContext 32 • 普通の static フィールドの利用 • async メソッドは通常、平行ないし並列で実行される。 •
ある async control flow で書き換えた static フィールドは、別の flow で書き換えられてしまう。 • 競合を避けるためには1つのメソッドしか実行することができない。 • つまり ambient data の渡し方として成立しない。 • Thread local フィールドの利用 • [ThreadStatic] 属性が適用された static フィールドや ThreadLocal<T> で実現可能 • 同期メソッドであれば問題なし。 • 一方で、async メソッドは await した Task インスタンスが完了状態ではなかった場合 サスペンドが発生するが、それが完了した際の continuation はどのスレッドで 実行されるか分からない。 • つまり、アクセスした ambient data が同一の async control flow のものとは限らない。 Ambient data の渡し方のパターンと async メソッドでの問題点 -
ExecutionContext 33 • 普通の static フィールドの利用 • async メソッドは通常、平行ないし並列で実行される。 •
ある async control flow で書き換えた static フィールドは、別の flow で書き換えられてしまう。 • 競合を避けるためには1つのメソッドしか実行することができない。 • つまり ambient data の渡し方として成立しない。 • Thread local フィールドの利用 • [ThreadStatic] 属性が適用された static フィールドや ThreadLocal<T> で実現可能 • 同期メソッドであれば問題なし。 • 一方で、async メソッドは await した Task インスタンスが完了状態ではなかった場合 サスペンドが発生するが、それが完了した際の continuation はどのスレッドで 実行されるか分からない。 • つまり、アクセスした ambient data が同一の async control flow のものとは限らない。 Ambient data の渡し方のパターンと async メソッドでの問題点 これらの問題を解決するのが ExecutionContext -
ExecutionContext 34 .NET Framework • LogicalCallContext や SecurityContext など様々なコンテキストを内包し、 それら全てのコンテキストをフローさせていた。
• Mutable だった。 .NET Core • AsyncLocal<T> を格納し、それをフローするだけのものになった。 • それ以外のものは綺麗さっぱり消えた。 • Immutable になった。 ExecutionContext の役割 このコンテキストの上に ambient data を乗っけて async control flow 間でデータをフローさせる ※ 本セッションでは NET Framework との呼び分けのため .NET Core に .NET 5,6,7 も含む事とします。 -
ExecutionContext 35 .NET Framework • LogicalCallContext や SecurityContext など様々なコンテキストを内包し、 それら全てのコンテキストをフローさせていた。
• Mutable だった。 .NET Core • AsyncLocal<T> を格納し、それをフローするだけのものになった。 • それ以外のものは綺麗さっぱり消えた。 • Immutable になった。 ExecutionContext の役割 このコンテキストの上に ambient data を乗っけて async control flow 間でデータをフローさせる 我々は ExecutionContext に直接 ambient data を格納したりはしない。 AsyncLocal<T> を経由する事。 ※ 本セッションでは NET Framework との呼び分けのため .NET Core に .NET 5,6,7 も含む事とします。 -
Compiler Transform 37 • AsyncTaskMethodBuilder.Start メソッドは何をやっているのか? • 実質的には、ステートマシンの MoveNext を呼んでいるだけ。
• 何故直接 ステートマシンの MoveNext メソッドを呼ばないのか? • Start メソッドでは、細かいけど大事な事をやっている。それはなにか? • ExecutionContext を理解する必要があります。 AsyncTaskMethodBuilder.Start(ref stateMachine) 再掲 -
AsyncTaskMethodBuilder.Start(ref stateMachine) 41 実際には… ステートマシンの MoveNext を呼ぶ前に ExecutionContext をキャプチャしておき 最後に、事前にキャプチャしておいた
ExecutionContext をリストアする 何故こんな事を しているのか? -
Compiler Transform 43 • メソッドの呼び出し元へ ambient data のリークを防ぐため。 何故こんな事をしているのか? ログインしたユーザ情報を
AsyncLocal<T> を用いて ExecutionContext に格納 -
Compiler Transform 44 • メソッドの呼び出し元へ ambient data のリークを防ぐため。 何故こんな事をしているのか? ログインしたユーザ情報を
AsyncLocal<T> を用いて ExecutionContext に格納 builder.Start() の finaly で呼び出し時点の ExecutionContext がリストアされて いなかった場合、変更された ExecutionContext から ambient data がリーク -
MoveNext 46 1. ステートマシン/ビルダーの構造体が初期化され 2. ビルダーの builder.Start(ref stateMachine) メソッドが呼ばれ 3.
ステートマシンの MoveNext メソッドが呼び出される async メソッドが実行されたら いまここ -
MoveNext 47 • 開発者がメソッドに記述したあったロジックを すべて含み、かつ多くの変更も含む。 • どこまで状態が進んだか保存するフィールドと それに応じてジャンプする switch 等。
• MoveNext メソッドは、全ての作業が終了したら async メソッドから返される Task オブジェクトを 必ず完了させる責務を負う。 • Task.IsCompleted が true の状態 MoveNext の中身 (try block 略) 終了時 or 例外発生時に ビルダーの SetException/SetResult を 経由し Task を完了状態にする -
MoveNext 48 • 先頭の source.ReadAsync は Task<int> を返す。 • この
Task<int> オブジェクトの状態をみて分岐 • 完了している場合は GetResult() で結果を 取得して同期的(サスペンドなし)に継続。 • 完了してない場合は continuation をフックする 必要がある。 MoveNext の中身 (try block の一部) -
MoveNext 49 • 先頭の source.ReadAsync は Task<int> を返す。 • この
Task<int> オブジェクトの状態をみて分岐 • 完了している場合は GetResult() で結果を 取得して同期的(サスペンドなし)に継続。 • 完了してない場合は continuation をフックする 必要がある。 MoveNext の中身 (try block の一部) 実際には Task オブジェクトに 直接アクセスはせず、 awaiter を介して状態の確認と continuation の登録を行う -
MoveNext 50 • 先頭の source.ReadAsync は Task<int> を返す。 • この
Task<int> オブジェクトの状態をみて分岐 • 完了している場合は GetResult() で結果を 取得して同期的(サスペンドなし)に継続。 • 完了してない場合は continuation をフックする 必要がある。 MoveNext の中身 (try block の一部) 実際には Task オブジェクトに 直接アクセスはせず、 awaiter を介して状態の確認と continuation の登録を行う awaiter パターンに従えば何でも (Task 以外でも) await できる -
MoveNext 51 • 先頭の source.ReadAsync は Task<int> を返す。 • この
Task<int> オブジェクトの状態をみて分岐 • 完了している場合は GetResult() で結果を 取得して同期的(サスペンドなし)に継続。 • 完了してない場合は continuation をフックする 必要がある。 MoveNext の中身 (try block の一部) 実際には Task オブジェクトに 直接アクセスはせず、 awaiter を介して状態の確認と continuation の登録を行う awaiter パターンに従えば何でも (Task 以外でも) await できる -
MoveNext 52 • 先頭の source.ReadAsync は Task<int> を返す。 • この
Task<int> オブジェクトの状態をみて分岐 • 完了している場合は GetResult() で結果を 取得して同期的(サスペンドなし)に継続。 • 完了してない場合は continuation をフックする 必要がある。 MoveNext の中身 (try block の一部) 実際には Task オブジェクトに 直接アクセスはせず、 awaiter を介して状態の確認と continuation の登録を行う awaiter パターンに従えば何でも (Task 以外でも) await できる -
MoveNext 53 • 先頭の source.ReadAsync は Task<int> を返す。 • この
Task<int> オブジェクトの状態をみて分岐 • 完了している場合は GetResult() で結果を 取得して同期的(サスペンドなし)に継続。 • 完了してない場合は continuation をフックする 必要がある。 MoveNext の中身 (try block の一部) 実際には Task オブジェクトに 直接アクセスはせず、 awaiter を介して状態の確認と continuation の登録を行う awaiter パターンに従えば何でも (Task 以外でも) await できる ビルダーを介して continuation をフック -
MoveNext 54 • awaiter と呼ばれるものは、以下のような interface を実装している。 • Task 完了時に呼び出される
continuation を OnCompleted / UnsafeOnCompletedで渡す。 Awaiter のとりあえずコレだけは押さえて欲しいポイント! -
MoveNext • サスペンドが発生するまで • ステートマシンとビルダー の 双方はスタックに存在する • サスペンドするためには •
ステートマシンとビルダーをスタックからヒープにコピーしないといけない…! • なぜ? • 使用中のスタックは同一のスレッド上で実行される、 現在のフローとは全く無関係な後続の作業に使用される。明け渡さないといけない。 • 明け渡した上で、正しく continuation が実行できないといけない。 • サスペンド発生後 • continuation では、ヒープ上にコピーされた ステートマシンの MoveNext メソッドを呼び出す必要がある。 builder.AwaitUnsafeOnCompleted でサスペンドが起きる前と後 ExecutionContext がついて回る -
MoveNext • ステートマシンは、ExecutionContext に格納された ambient data を サスペンドされた時点で確実にキャプチャし、再開時点で復元する必要がある。 • つまり
continuation にキャプチャした ExecutionContext を取り込む必要がある。 MoveNext と ExecutionContext -
MoveNext • ステートマシンは、ExecutionContext に格納された ambient data を サスペンドされた時点で確実にキャプチャし、再開時点で復元する必要がある。 • つまり
continuation にキャプチャした ExecutionContext を取り込む必要がある。 MoveNext と ExecutionContext ステートマシンの MoveNext を指す デリゲートを作るだけでは足りない -
MoveNext • ステートマシンは、ExecutionContext に格納された ambient data を サスペンドされた時点で確実にキャプチャし、再開時点で復元する必要がある。 • つまり
continuation にキャプチャした ExecutionContext を取り込む必要がある。 MoveNext と ExecutionContext ステートマシンの MoveNext を指す デリゲートを作るだけでは足りない 毎回デリゲートを作成した場合 毎回デリゲートの allocation + boxing コストがかかる -
MoveNext • ステートマシンは、ExecutionContext に格納された ambient data を サスペンドされた時点で確実にキャプチャし、再開時点で復元する必要がある。 • つまり
continuation にキャプチャした ExecutionContext を取り込む必要がある。 MoveNext と ExecutionContext ステートマシンの MoveNext を指す デリゲートを作るだけでは足りない 毎回デリゲートを作成した場合 毎回デリゲートの allocation + boxing コストがかかる 初回のサスペンド時にのみ 構造体を boxing し、 以降は boxing されたヒープにある オブジェクトを MoveNext を 行う対象として使用し、 適切なタイミングで ExecutionContext をキャプチャし、 再開時にはその ExecutionContext を 使用する必要がある -
MoveNext • ステートマシンは、ExecutionContext に格納された ambient data を サスペンドされた時点で確実にキャプチャし、再開時点で復元する必要がある。 • つまり
continuation にキャプチャした ExecutionContext を取り込む必要がある。 MoveNext と ExecutionContext ステートマシンの MoveNext を指す デリゲートを作るだけでは足りない 毎回デリゲートを作成した場合 毎回デリゲートの allocation + boxing コストがかかる 初回のサスペンド時にのみ 構造体を boxing し、 以降は boxing されたヒープにある オブジェクトを MoveNext を 行う対象として使用し、 適切なタイミングで ExecutionContext をキャプチャし、 再開時にはその ExecutionContext を 使用する必要がある 複雑! -
MoveNext • ステートマシンは、ExecutionContext に格納された ambient data を サスペンドされた時点で確実にキャプチャし、再開時点で復元する必要がある。 • つまり
continuation にキャプチャした ExecutionContext を取り込む必要がある。 MoveNext と ExecutionContext ステートマシンの MoveNext を指す デリゲートを作るだけでは足りない 毎回デリゲートを作成した場合 毎回デリゲートの allocation + boxing コストがかかる 初回のサスペンド時にのみ 構造体を boxing し、 以降は boxing されたヒープにある オブジェクトを MoveNext を 行う対象として使用し、 適切なタイミングで ExecutionContext をキャプチャし、 再開時にはその ExecutionContext を 使用する必要がある 複雑! .NET Framework から .NET Core で このあたりがとても改善され パフォーマンスが向上している -
MoveNext 62 1. ExecutionContext.Capture()を使って、現在の ExecutionContext (以後 ec)を取得。 2. Ec と
boxing されたステートマシンをラップする MoveNextRunner (class) を new. 3. MoveNextRunner の Run メソッドに対するデリゲート(Action) を new. • キャプチャした ec 上でステートマシンの MoveNext を呼べるようになる 4. 初回サスペンド時のみ、struct であるステートマシンを boxing してヒープに。 • ビルダーはステートマシンをフィールドに持ち、 ステートマシンはビルダーをフィールドに持つ。 スタックからヒープに双方をコピーする都合上、参照関係を正しく設定する必要がある。 このためにかなり複雑な操作を行う。 5. 最後に continuation を表す Action デリーゲートを用意し awaiter の UnsafeOnCompleted メソッドに渡します。 .NET Framework での builder.AwaitUnsafeOnCompleted -
MoveNext 63 • 右のようなシンプルなコードで パフォーマンスを確認。 • 1000 x 1000 で合計
100 万回の await Task.Yield() が走る。 .NET Framework での振る舞いをプロファイラで見てみる -
MoveNext 65 .NET Framework での振る舞いをプロファイラで見てみる .NET Framework での ExecutionContext は
mutable. Capture されるタイミングで毎回コピーが発生 (100万回) -
MoveNext 66 .NET Framework での振る舞いをプロファイラで見てみる .NET Framework での ExecutionContext は
mutable. Capture されるタイミングで毎回コピーが発生 (100万回) UnsafeOnCompleted に渡す デリゲートを作成 (100万回) -
MoveNext 67 .NET Framework での振る舞いをプロファイラで見てみる .NET Framework での ExecutionContext は
mutable. Capture されるタイミングで毎回コピーが発生 (100万回) UnsafeOnCompleted に渡す デリゲートを作成 (100万回) サスペンド毎に MoveNextRunner を allocate (100万回) -
MoveNext 68 .NET Framework での振る舞いをプロファイラで見てみる .NET Framework での ExecutionContext は
mutable. Capture されるタイミングで毎回コピーが発生 (100万回) UnsafeOnCompleted に渡す デリゲートを作成 (100万回) ExecutionContext に含まれる 別のコンテキストのコピー(100万回) サスペンド毎に MoveNextRunner を allocate (100万回) -
MoveNext 69 .NET Framework での振る舞いをプロファイラで見てみる .NET Framework での ExecutionContext は
mutable. Capture されるタイミングで毎回コピーが発生 (100万回) UnsafeOnCompleted に渡す デリゲートを作成 (100万回) サスペンド毎に MoveNextRunner を allocate (100万回) ExecutionContext に含まれる 別のコンテキストのコピー(100万回) Task.Yield() は ThreadPool に work item を queueing している。 Yield 毎の操作を表現するために allocate (100 万回) -
MoveNext 70 .NET Framework での振る舞いをプロファイラで見てみる .NET Framework での ExecutionContext は
mutable. Capture されるタイミングで毎回コピーが発生 (100万回) UnsafeOnCompleted に渡す デリゲートを作成 (100万回) サスペンド毎に MoveNextRunner を allocate (100万回) ExecutionContext に含まれる 別のコンテキストのコピー(100万回) Task.Yield() は ThreadPool に work item を queueing している。 Yield 毎の操作を表現するために allocate (100 万回) 他にもいろいろ allocation が発生。 渋いポイントが多い。 一方で .NET Core はどうだろうか? -
MoveNext 73 .NET Core での振る舞いをプロファイラで見てみる とんでもないレベルの改善…! .NET Framework : 500
万以上の allocation (~145MB) .NET Core : 1000 程度の allocation (~109KB) -
MoveNext 74 1. .NET Coreでは、ExecutionContext が immutable に。 キャプチャの度にコピーが不要 (参照を渡すだけで
OK になった) 1. AsyncLocal<T> に値を設定する等したタイミングで ExecutionContext が新たに作成される。 キャプチャの方が高頻度に叩かれるので、キャプチャ時ではなく変更時にコストを寄せた。 2. LogicalCallContext は .NET Core から消えた。 1. .NET Core での ExecutionContext の役割は AsyncLocal<T> のストレージ。それだけ。 それ以外のコンテキストは消えたので無駄なコストがかからない。 .NET Framework から .NET Core での改善の要因 (の一部) -
MoveNext 75 1. .NET Coreでは、ExecutionContext が immutable に。 キャプチャの度にコピーが不要 (参照を渡すだけで
OK になった) 1. AsyncLocal<T> に値を設定する等したタイミングで ExecutionContext が新たに作成される。 キャプチャの方が高頻度に叩かれるので、キャプチャ時ではなく変更時にコストを寄せた。 2. LogicalCallContext は .NET Core から消えた。 1. .NET Core での ExecutionContext の役割は AsyncLocal<T> のストレージ。それだけ。 それ以外のコンテキストは消えたので無駄なコストがかからない。 .NET Framework から .NET Core での改善の要因 (の一部) Action、MoveNextRunner、ステートマシンなどの allocation は??? -
MoveNext 76 1. .NET Coreでは、ExecutionContext が immutable に。 キャプチャの度にコピーが不要 (参照を渡すだけで
OK になった) 1. AsyncLocal<T> に値を設定する等したタイミングで ExecutionContext が新たに作成される。 キャプチャの方が高頻度に叩かれるので、キャプチャ時ではなく変更時にコストを寄せた。 2. LogicalCallContext は .NET Core から消えた。 1. .NET Core での ExecutionContext の役割は AsyncLocal<T> のストレージ。それだけ。 それ以外のコンテキストは消えたので無駄なコストがかからない。 .NET Framework から .NET Core での改善の要因 (の一部) Action、MoveNextRunner、ステートマシンなどの allocation は??? .NET Core でどのように 動いているかを 知らないと分からない。 -
MoveNext 78 • builder.AwaitUnsafeOnCompleted で continuation が登録される。 ここまでは .NET Framework,
.NET Core で共通。 AwaitUnsafeOnCompleted の内部実装が大幅に異なる。 MoveNext 内でサスペンド時に何が起きていたか?(復習) -
MoveNext 79 1. まず、ExecutionContext を capture. 2. その後 m_task が
null なら AsyncStateMachineBox<TStateMachine> を allocate. .NET Core のビルダーと AwaitUnsafeOnCompleted -
MoveNext 80 1. まず、ExecutionContext を capture. 2. その後 m_task が
null なら AsyncStateMachineBox<TStateMachine> を allocate. .NET Core のビルダーと AwaitUnsafeOnCompleted フィールドが一つあるだけ…! -
MoveNext 81 1. まず、ExecutionContext を capture. 2. その後 m_task が
null なら AsyncStateMachineBox<TStateMachine> を allocate. .NET Core のビルダーと AwaitUnsafeOnCompleted フィールドが一つあるだけ…! 圧倒的改善を 実現するための鍵 -
MoveNext 82 1. まず、ExecutionContext を capture. 2. その後 m_task が
null なら AsyncStateMachineBox<TStateMachine> を allocate. .NET Core のビルダーと AwaitUnsafeOnCompleted フィールドが一つあるだけ…! 基底型に注目! 圧倒的改善を 実現するための鍵 -
MoveNext 83 • その名の通り、スタックに存在する構造体をヒープに box 化するためのクラス • 重要な事は、IAsyncStateMachine として boxing
するのではなく、 TStateMachine として強く型付けされたフィールドとしてヒープに。 • それでいながら、Task そのもの (基底型から分かる通り) • Action と ExecutionContext も フィールドに格納。 MoveNextRunner はいらない子に。 • 新たに ExecutionContext を キャプチャしてもフィールドを 書き換えればいいだけ。 AsyncStateMachineBox<TStateMachine> -
MoveNext 84 • awaiter に continuation を登録する際、 AsyncStateMachineBox の MoveNext
を 叩けるようにする。 • 通常であれば、UnsafeOnCompleted に デリゲートを渡して登録するわけだが、 その際には MoveNext を指すデリゲートを 初回のみ作成しキャッシュしておく。 • Continuation 発火時、 AsyncStateMachineBox の MoveNext は ステートマシンの MoveNext を 呼び出す前に ExecutionContext の リストアを行う。 awaiter と continuation と AsyncStateMachineBox -
MoveNext 87 .NET Core での振る舞いをプロファイラで見てみる (2回目) awaiter に登録するための デリゲートの allocation
が 発生していないぞ?何故?? Action continuation が 求められている -
MoveNext 88 • 非同期インフラストラクチャは、Task や TaskAwaiter などのコアとなる 型について既知です。 • さらにそれらは内部アクセス権を持っています。
• public に定義されたルールに従う必要はない…! • 非同期インフラストラクチャが既知の awaiter については より無駄のない経路 (デリゲートを作成しない) を辿る事が出来る。 IAsyncStateMachineBox に対して最適化されています。 -
MoveNext 89 • 非同期インフラストラクチャは、Task や TaskAwaiter などのコアとなる 型について既知です。 • さらにそれらは内部アクセス権を持っています。
• public に定義されたルールに従う必要はない…! • 非同期インフラストラクチャが既知の awaiter については より無駄のない経路 (デリゲートを作成しない) を辿る事が出来る。 IAsyncStateMachineBox に対して最適化されています。 • await したい対象のオブジェクトが GetAwaiter メソッドで awaiter を返す事 • awaiter には OnCompleted / UnsafeOnCompleted メソッドが必要でどちらも continuation を Action として受け取る。 Public に定義されたルール(の一部) -
MoveNext 90 • Task.Yield() は YieldAwaitable を返す。 • YieldAwaitable は
GetAwaiter で YieldAwaiter を返す。 Task.Yield() の最適化例 AwaitUnsafeOnCompleted で box を受け取り ThreadPool に デリゲートではなく Box をそのまま渡す 非同期インフラストラクチャは デリゲートを作成せずに box の MoveNext を 直接呼び出すようになるので Allocation が発生していない -
まとめ 91 • コールバックによる continuation の解決の鍵は C# のイテレータにあった • C#
コンパイラのイテレータと async/await をサポートするための ロジックの約95%は共有されている • async/await で重要な型 • ExecutionContext • AsyncTaskMethodBuilder • IAsyncStateMachine • IAsyncStateMachineBox • AsyncStateMachineBox • .NET Framework から .NET Core で劇的な進化を遂げている。 ※ 本セッションでは NET Framework との呼び分けのため .NET Core に .NET 5,6,7 も含む事とします。 -
References 92 Stephen Toub 氏 によるメチャ面白記事。 恐らく async/await の内部 (.NET
7 時点) についての解説という点で 一番詳しく、一番分かりやすい。 How Async/Await Really Works in C# https://devblogs.microsoft.com/dotnet/how-async-await-really-works/