Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

C# 8.0 非同期ストリーム

913 views

Published on

.NET Conf Tokyo 2019 にて登壇。
https://vsuc.connpass.com/event/146588/
<

Published in: Technology
  • Be the first to comment

C# 8.0 非同期ストリーム

  1. 1. C# 8.0 非同期ストリーム 岩永 信之
  2. 2. 今日の話 • C# 8.0の非同期ストリーム • 非同期メソッドやTaskクラス周りの歴史 • パフォーマンス改善
  3. 3. C# 8.0の他の機能 • Preview版の頃から割と安定していた機能は4月の登壇を参照 • null許容参照型は先月の登壇を参照 Visual Studio 2019 Launch (https://connpass.com/event/122145/) C# 8.0 Preview in Visual Studio 2019 (16.0) (https://www.slideshare.net/ufcpp/c-80-preview-in-visual-studio-2019-160) Visual Studio Users Community Japan #1 (https://vsuc.connpass.com/event/143114/) C# 8.0 null許容参照型 (https://www.slideshare.net/ufcpp/c-80-null)
  4. 4. C# 8.0の新機能: 非同期ストリーム
  5. 5. 非同期ストリームとは • 非同期で複数のデータを受け渡し • 非同期メソッドでyieldを使える = 複数のデータを非同期に返す • foreachの非同期版が使える = 複数のデータを非同期に受け取る await Task.Delay(1); yield return 1; awaitとyieldの混在 await foreach (var item in source) { } await foreach構文
  6. 6. 非同期ストリームの例 • gRPCを例にして非同期ストリームを紹介 • サンプル: NetCoreGrpc • 背景 • gRPCは非同期ストリームを送受信する想定を持ってる • streamキーワードを付けると非同期ストリームになる • ASP.NET Core 3.0はgRPCに対応 • 既存のGoogle.Protobufパッケージを使っていそう • C# 8.0の非同期ストリームに対応した拡張メソッドも提供 • gRPCのstreamに対してawait foreachを使える
  7. 7. 非同期ストリームの例(gRPC) • サンプル(proto定義) • Microsoft独自なことはしておらず、普通のProtocol Buffers service Sample { rpc GetValues(stream SampleRequest) returns (stream SampleResponse); } message SampleRequest { int32 bits = 1; int32 length = 2; } message SampleResponse { repeated int32 values = 1; } stream って付けておくと 非同期に複数のデータを送れる 例として • bits桁の数値をlength個要求して • リスト(repeated)で返してもらう
  8. 8. 非同期ストリームの例(コード生成) • protoからC#コードが生成される • サーバー側 (生成結果のクラスを派生して使う) • クライアント側 (生成結果のクラスをnewして使う) public class SampleService : Sample.SampleBase { public override async Task GetValues( IAsyncStreamReader<SampleRequest> requestStream, IServerStreamWriter<SampleResponse> responseStream, ServerCallContext context) ... var client = new SampleClient(channel); var call = client.GetValues(); call.RequestStream ... call.ResponseStream ... 送信側 受信側 受信側 送信側
  9. 9. 非同期ストリームの例(送信側) • 一定時間ごとにデータを要求 async Task sender() { for (int i = 1; i < 32; i++) { await call.RequestStream.WriteAsync( new SampleRequest { Bits = i, Length = i }); await Task.Delay(500); } await call.RequestStream.CompleteAsync(); } とりあえずちょっと遅延を挟んでおく 複数のデータを送信 (stream) もうこれ以上送るデータが ないことを伝える
  10. 10. 非同期ストリームの例(サーバー) • 要求が来るたびにリストを作って返す var rand = new Random(); await foreach (var req in requestStream.ReadAllAsync()) { var mask = (1 << req.Bits) - 1; var len = req.Length; var res = new SampleResponse(); for (int i = 0; i < len; i++) res.Values.Add(rand.Next() & mask); await responseStream.WriteAsync(res); } 1要求受け取るたびに そのリストを送る len要素のリストを作って
  11. 11. 非同期ストリームの例(受信側) • 複数回非同期でリストが届いて、リストの1要素ずつ返す async IAsyncEnumerable<int> receiver() { await foreach (var res in call.ResponseStream.ReadAllAsync()) { foreach (var value in res.Values) { yield return value; } } } 複数のデータを受信 (stream) 受信のたびに複数の値 (repeated) リストの1要素ずつ返す
  12. 12. 非同期ストリームの例(受信側) • 複数回非同期でリストが届いて、リストの1要素ずつ返す async IAsyncEnumerable<int> receiver() { await foreach (var res in call.ResponseStream.ReadAllAsync()) { foreach (var value in res.Values) { yield return value; } } } 昔だったら • void receiver(IObservable<int> results) • → foreachで使いにくい • Task<IEnumerable<int>> receiver() • → 1要素ずつ返せない(List<T>とかのバッファー必須)
  13. 13. C# 8.0の言語構文・ライブラリ的な説明 • 非同期メソッドの拡張 • awaitとyieldの混在 • async修飾子を付ける • 戻り値はIAsyncEnumerable<T>型 • 非同期foreach • await foreach (var x in asyncStream) • IAsyncEnumerable<T> (と同じ名前のメソッドを持つ型)を受け付ける • (おまけで)非同期using • await using (var x = asyncResource) • IAsyncDisposable (と同じ名前のメソッドを持つ型)を受け付ける
  14. 14. 要求されるフレームワーク • 非同期ストリームにはIAsyncEnumerable<T>型などが必須 • .NET Standard2.1/.NET Core 3.0では標準提供 • 古いフレームワーク向けのNuGetパッケージ提供あり • Microsoft.Bcl.AsyncInterfaces • netstandard2.0/net461でも使える(Unityでも使える) • ※非同期ストリームがらみだけ異例 • 最近の方針としてはC#と.NETの世代をそろえて使ってほしそう • 古いフレームワークで最新のC#を使うのは「わかってる人が勝手にやって」 • Range/Indexなどは公式ライブラリ提供なし • 非同期ストリームがらみはそれだけ需要が高い
  15. 15. IAsyncEnumerable<T>インターフェイス • IEnumerable<T>の非同期版 public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator( CancellationToken cancellationToken = default); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { T Current { get; } ValueTask<bool> MoveNextAsync(); } • 名前にAsyncが入ってる • ジェネリック版のみ • CancellationTokenをオプションで受け付ける • 戻り値がValueTask • Resetメソッドはない 同期版との差
  16. 16. 非同期foreach • IAsyncEnumerable<T>に対するforeach • 仕組みは同期版とほぼ同じ • パターンベース • (同名のメソッドを持って いればインターフェイス 実装は不要) await foreach (var x in s) { ... } var e = items.GetAsyncEnumerator(); try { while (await e.MoveNextAsync()) { int item = e.Current; ... } } finally { if (e != null) { await e.DisposeAsync(); } } • 呼ぶメソッドがAsyncで • awaitが付く
  17. 17. IAsyncDisposableインターフェイス • IDisposableの非同期版 public interface IAsyncDisposable { ValueTask DisposeAsync(); } • 名前にAsyncが入ってる • 戻り値がValueTask 同期版との差
  18. 18. 非同期using • IAsyncDisposableに対するusing • これも仕組みは同期版とほぼ同じ • (同期版と違って) パターンベース • (同名のメソッドを持って いればインターフェイス 実装は不要) await using (d) { ... } try { ... } finally { await d.DisposeAsync(); } • 呼ぶメソッドがAsyncで • awaitが付く
  19. 19. 非同期メソッドの拡張 • 非同期メソッド中にyieldを書けるように • これまでの非同期メソッド同様、async修飾子を付ける • これまでのイテレーター同様、メソッド中にyieldを書く • 戻り値はIAsyncEnumerable<T>である必要あり async IAsyncEnumerable<int> AsyncIterator() { await Task.Delay(1); yield return 1; yield break; } 要するに、非同期メソッド とイテレーターの混在
  20. 20. 補足: (同期)イテレーター • yield returnの展開結果(疑似コード) • 状態記録 → 中断 → 再開 _state = State1; Current = x; return true; case State1: yield return x; どこまで実行したか記録 IEnumerator<T>.Currentに値をセット いったん処理を中断 次にIEnumerator<T>.MoveNext が呼ばれた時の再開場所
  21. 21. 補足: await演算子 • awaitの展開結果(疑似コード) • 状態記録 → 中断 → 再開 _awaiter = task.GetAwaiter(); if (!_awaiter.IsCompleted) { _state = State1; _awaiter.OnComplete(自分自身); return; } case State1: var result = _awaiter.GetResult(); var result = await task; どこまで実行したか記録 task完了時に呼びなおしてもらう いったん処理を中断 OnCompleteが呼ばれた時の再開場所 結果の受け取り この辺りの構造がyield return とまったく同じ
  22. 22. 非同期イテレーター • 非同期メソッド中でのyield returnの展開結果(疑似コード) • 状態記録 → 中断 → 再開 (根底にある仕組みが同じだから混ぜれる) var result = _awaiter.GetResult(); _state = State1; Current = result; _promise.TrySetResult(true); return; case State1: var result = await task; yield return result; どこまで実行したか記録 Currentに値をセット いったん処理を中断 次にMoveNextAsyncが呼ばれた時の再開場所 awaitの結果を受け取り 1つ前のMoveNextAsyncを完了させる
  23. 23. パフォーマンス • Q. なぜ非同期にしたいか? • A. パフォーマンスを改善したい • Taskの生成などのオーバーヘッドがネックになってはいけない • もしC# 5.0世代の技術でやろうとすると… interface IAsyncEnumerator<out T> { T Current { get; } Task<bool> MoveNextAsync(); } _promise = new TaskCompletionSource<T>(); ... _promise.TrySetResult(true); MoveNextAsyncのたびに Taskクラスのインスタンスができる TaskCompletionSourceクラスは 再利用ができないので毎回new
  24. 24. パフォーマンスに関する前提 • 受信側サンプルより再掲: • 本当に非同期処理が必要なことは少ない • ReadAllAsyncの方だけ非同期 • Values側は同期 • ReadAllAsyncもバッファリングが効けば同期になることがある • IAsyncEnumerator<T>.MoveNextAsyncの呼ばれ方 • 1度に1人だけがawaitする • 戻り値のValueTaskは1回限りGetResultが呼ばれる await foreach (var res in call.ResponseStream.ReadAllAsync()) foreach (var value in res.Values) yield return value;
  25. 25. パフォーマンス改善のために • ValueTask<T>構造体 • C# 7.0/.NET Core 2.0世代 • IValueTaskSource<T>インターフェイス • C# 7.2/.NET Core 2.1世代 • ManualResetValueTaskSourceCore<T>構造体 • C# 8.0/.NET Core 3.0世代 ちょっと歴史を振り返りつつ説明
  26. 26. Task周りの歴史 Taskクラス初導入から非同期ストリームに至るまで
  27. 27. 時系列 2012 2013 2014 2015 2016 2017 2018 2019 Windows 8 WinRT C# 5.0 Microsoft CEO交代 Roslyn公開 (オープンソース化) C# 6.0 C# 7.2 C# 8.0C# 7.0 .NET Framework 4.5 .NET Core 1.1 .NET Core 2.0 .NET Core 2.1 .NET Core 3.0 async/await ValueTask IValueTaskSource ManualResetValueTaskSourceCore IAsyncEnumerable Task 一般非同期戻り値 非同期ストリーム
  28. 28. 先史時代 (2010年頃) before “await” Taskクラス
  29. 29. 課題: マルチコア化 • マルチコアCPUの普及 • 高クロック化に限界 (サイズが量子力学的な世界に突入) • 単体で速くできないので並列に並べるしかない • コア間のコミュニケーション コストが課題 • 「N個あればN倍」とはならない • 他のコアの動作を邪魔をしてはいけない • 暇になっているコアをなくさないといけない
  30. 30. Work stealing • 小さいタスクを大量にさばくにはスレッドは重たい • 新規作成・スレッド間のコンテキスト切り替えの負担が大きい • コア数分のスレッドを事前に立てておく • 各スレッドにキューを持つ(ローカル キュー) • タスクは1度キューに詰める • ローカルでやることがなくなったら他のスレッドのキューからタスク を奪って(stealing)実行する • Work stealingって呼ぶ • 効率のいい実装方法がいろいろ研究されてる • (JavaとかGoとかでも同時期に)
  31. 31. 当時の.NET周り • Windows 7 (2009/9リリース) • User Mode Scheduler • Work stealing的なことをやってる並列処理API • .NET Framework 4 (2010/4) • Taskクラス追加 • Parallelクラス追加 • ThreadPoolがWork stealing実装になって効率化 • 当時の課題は「マルチコアを使い切ること」 • CPUをフルに使うような計算主体
  32. 32. Taskクラス(継続呼び出し) • 同期だと と書けるものがあったとして • Task以前: Begin/Endのペア • Task以後: ContinueWith(継続呼び出し) x.BeginM(state => { var result = x.EndM(state); }); x.MAsync().ContinueWith(t => { var result = t.Result; }); var result = x.M(); 匿名関数がなかった頃は もっと大変だった
  33. 33. C# 5.0世代 (2012年頃) “await”元年
  34. 34. 課題: I/O-bound • CPUの高速化に対して… • 相対的にストレージ アクセスが遅くなった • CPUより3・4桁遅い • ネットワークを使うことが多くなった • 秒単位で待たされることもざら • ただ待ってる時間が増える • 待ち時間にフリーズすると印象が悪い • 待ってる間に他のことをしたい • せめて、マウスは動いてほしいし、インジケーターを出したい I/O-bound (CPUの外の世界との 入出力がネック)
  35. 35. 当時の.NET周り • Windows 8 (2012/10) • WinRT (新API)は非同期前提 • 同期処理でフリーズされるくらいなら、いっそ同期APIを用意しない • I/Oは軒並み非同期APIのみ提供 • .NET Framework 4.5 • 非同期I/OにTaskクラスを使うように • Stream.ReadAsync, HttpClient.GetAsync, ... • C# 5.0 (2012/8) • 非同期メソッド導入 • この当時は戻り値にTaskクラスしか使えなかった
  36. 36. 非同期メソッド • 同期と非同期で同じような書き方ができる • I/Oを非同期にしたいだけであって、やりたいことは同期と同じ • やりたいことが同じなら書き方は同じでありたい void M() { var result = x.N(); ... } async Task MAsync() { var result = await x.NAsync(); ... } 同期 非同期 • 非同期処理の前にawaitを付けるだけ • (習慣上)メソッド名にAsync語尾を付ける
  37. 37. Taskの用途が変わった • 先史時代: マルチコアをフルに使いたい • Task.WhenAllやParallel.Forが主力 • await後: むしろI/O待ちに使われるように • 1つのプログラムでCPUをフルに使いきることはあまりない • 書きたいコード自体は同期の頃と同じ • Taskの利用場面の大半がawaitに
  38. 38. 余談: C# 5.0→6.0の開発体制 (2012~2015頃) WindowsからAzureに オープン化
  39. 39. C# 5.0開発進行上の成約 • WinRTのための非同期メソッド • Windows 8に合わせてC# 5.0を出すのが必須だった • Windowsがクローズだったので、C# 5.0もクローズなところがあった • 非同期メソッドの初期設計もオープンにできないことが多かった 締め切り的にもフィードバック的にもなかなか 自由が利かない中でのawait導入 • 汎用性を持たせる余裕なし • 戻り値はTask, Task<T>に限る • awaitはGetAwaiterメソッドを直呼び
  40. 40. C# 6.0開発体制 • MicrosoftのCEO交代で一気にオープンに • C#コンパイラーも2014/4にオープンソース化(当時はcodeplex) • 2015/1にGitHub移行 • コンパイラーを1から作り直してたので言語構文的には停滞期 • .NET Coreも1.0は「まずは最低限動く」が目標 • 非同期メソッド周りもいったん据え置き • 計画としては早期からあった非同期ストリームの話も8.0までおあずけ • ASP.NET方面からのフィードバックが強くなる
  41. 41. C# 7.0世代 (2017年) .NET Core 2.0 徐々にパフォーマンス向上が命題に
  42. 42. パフォーマンス • .NET Core 2.Xはパフォーマンス改善の積み重ねだった • ガベコレ前提・安全性と生産性優先の言語の中ではだいぶ速くなった • パフォーマンス的にはC, C++, Rustの次くらいの位置には来れてる • ヒープ アロケーションをできるかぎり避ける方針に • ガベコレは十分速いけど、「ヒープを使わない」方がもっと速い • なので、非同期メソッドのTask<T>のアロケーションも問題に • ValueTask<T>構造体の導入 • 最初はNuGetパッケージで提供 • .NET Core 2.0から標準提供
  43. 43. ValueTask • ValueTask<T> = T or Task<T> な共用体 async ValueTask<int> MAsync() { if (9割9分) return 1; await Task.Delay(1); return 0; } 高確率で実際には同期処理 低確率で非同期処理 = T を返せばいい = Task<T> が必要 通信結果をキャッシュしたりバッファリング したり、こういう場面は結構な頻度で起こる (高確率でTask<T>のアロケーションを回避) struct ValueTask<T> { T _result; Task<T> _task; } 構造 用途の例
  44. 44. 非同期メソッド戻り値の汎用化 • C# 7.0で非同期メソッドの戻り値を汎用化 • Task<T>, Task以外の型を非同期メソッドの戻り値にできるように • パターン ベース(所定のメソッドさえ持っていれば型は問わない) • ただ、結構複雑なパターンだし、自前で書くことはほとんどない • なので説明割愛 • 気になる人は「AsyncMethodBuilder」で検索 • (C# 8.0世代ですら)実用上はほぼValueTaskのために入った機能
  45. 45. ValueTaskに掛かるオーバーヘッド • コスト • Task<T>とTを両方持つ上に、追加でちょっとフラグを持つ • (Tによれど大体)16バイト以上になる • 常に非同期な場合は単なるオーバーヘッド • 結局Task<T>クラスが作られた上で大きめの構造体コピーが発生 C# 7.0世代での問題 後に出てくる改善案が IValueTaskSource<T>
  46. 46. C# 7.2世代 (2018年) .NET Core 2.1 本格的にパフォーマンス向上が命題に
  47. 47. 非同期戻り値を汎用化するにあたって • おさらい • Task/Task<T>のアロケーションを避けたい • await用途の場合いくつか前提を置ける • 1度に1人だけがawaitする • 1回限りGetResultが呼ばれる • AsyncMethodBuilderを作るのはそれなりに大変 • 結局、ValueTaskに乗っかりたい • IValueTaskSouceインターフェイスを提供することに
  48. 48. IValueTaskSource<T> • 非同期メソッドの戻り値の求められる要件を満たす型 • ValueTask<T>の仕組みに乗っかれる • ValueTask<T>のコンストラクターに渡せる • AsyncMethodBuilderはValueTask<T>のものをそのまま使う public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); }
  49. 49. IValueTaskSource<T> • やりたいこと自体はTask<T>と同じ public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); } public class Task<TResult> { TaskStatus Status { get; } Task ContinueWith(Action<Task> continuation); TResult Result { get; } } ≒
  50. 50. IValueTaskSource<T> • パフォーマンスへの配慮あり public interface IValueTaskSource<out TResult> { ValueTaskSourceStatus GetStatus(short token); void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags); TResult GetResult(short token); } • 何回目の呼び出しかを弁別するための数値 • 1つのインスタンスを使いまわす前提 アロケーションを少しでも減らす
  51. 51. ValueTask<T>側の変化 • ValueTask<T> = T or Task<T> or IValueTaskSource<T> struct ValueTask<T> { T _value; object _task; short _token; } is演算子でTask<T> or IValueTaskSource<T>に分岐 何回目の呼び出しかを弁別するための数値 最初から完了しているときはTを直接渡す
  52. 52. ValueTask/IValueTaskSource (型引数なし) • ValueTask = Task or IValueTaskSource or 完了済み struct ValueTask { object _task; short _token; } is演算子でTask or IValueTaskSourceに分岐 nullだったら完了済み 何回目の呼び出しかを弁別するための数値 public interface IValueTaskSource { ValueTaskSourceStatus GetStatus(short token); void OnCompleted(Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags); void GetResult(short token); }
  53. 53. IValueTaskSourceを再利用 • インスタンスの再利用でアロケーションを減らす例 • 1つのインスタンス(_sourceに格納)を使いまわす • 1つ前のM()呼び出しが終わる前に重複でM()を呼ばない • ResetのたびにTokenを変える • TrySetResult時にTokenが不一致だと例外を出す ValueTaskSource _source = new ValueTaskSource(); ValueTask M() { _source.Reset(); return new ValueTask(_source, _source.Token); } どこか別の場所でTrySetResultする
  54. 54. TaskよりValueTaskの方が汎用的に • Task … 具象クラス。あまり変更の余地なし • ValueTask … これ自体は変更の余地なし • IValueTaskSrouceを渡せる • IValueTaskSrouceは自由に実装できる • どちらが推奨か interface X { Task M1(); ValueTask M2(); } インターフェイスみたいに 実装がどうなるかわからないとき、 TaskにすべきかValueTaskにすべきか
  55. 55. TaskよりValueTaskの方が汎用的に • Task … 具象クラス。あまり変更の余地なし • ValueTask … これ自体は変更の余地なし • IValueTaskSrouceを渡せる • IValueTaskSrouceは自由に実装できる • どちらが推奨か • 昔: ValueTaskにもコストはあるし、構造体は扱いにくいし • Task優位 • 今: ValueTaskの方が汎用になったし、実装次第では高パフォーマンス • ValueTask優位 ただし、構造体の扱いにくさは残ってる…
  56. 56. 扱いにくさ • 後入りなので… • Task戻り値をValueTask戻り値に変えると破壊的変更になる • TaskとValueTaskが混在しているときにWhenAnyとかが面倒 • 構造体なので… • box化に気を付けないといけない • ValueTask<T>とValueTaskに継承関係がない • それぞれに対して同じロジックを多重保守する必要あり • 組み合わせ爆発もあり得る void M(ValueTask a, ValueTask b); void M<T>(ValueTask a, ValueTask<T> b); void M<T>(ValueTask<T> a, ValueTask b); void M<T>(ValueTask<T> a, ValueTask<T> b); 引数の数nに対して 2 𝑛個のオーバーロード
  57. 57. C# 8.0世代(2019年) IValueTaskSourceが本腰 非同期ストリーム
  58. 58. IValueTaskSourceってどう実装するの? • IValueTaskSourceを使えと言われましても… • 汎用にできるといってもやることはほぼ常に同じ • スレッド安全でないとダメ • ちゃんと書くのは結構難しい • 共通処理を詰め込んだ構造体を提供 • ManualResetValueTaskSourceCore<T> • 自前のクラスにこの構造体のフィールドを持たせて、 IValueTaskSourceの実装に使える
  59. 59. ManualResetValueTaskSourceCoreの例 • IValueTaskSource<T>の処理をほぼ丸投げ class ValueTaskSource : IValueTaskSource<T> { private ManualResetValueTaskSourceCore<T> _core; public int GetResult(short token) => _core.GetResult(token); public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token); public void OnCompleted(Action<object> continuation, object state, short token, ...) => _core.OnCompleted(continuation, state, token, flags); public ValueTask<T> ValueTask => new ValueTask<T>(this, _core.Version); } フィールドに持って ほぼ丸投げ Reset(再利用)とかTrySetResult(完了)とかの タイミングだけ独自にコントロールしてやればOK
  60. 60. 非同期ストリームの中身 • ManualResetValueTaskSourceCore<T>を使って実装 await foreach (var x in s) { ... } while (await e.MoveNextAsync()) { int item = e.Current; ... var result = _awaiter.GetResult(); _state = State1; Current = result; _promise.TrySetResult(true); return; case State1: var result = await task; yield return result; 再掲・抜粋 戻り値がValueTask<T> 本当に非同期な時だけアロケーション ManualResetValueTaskSourceCore<T>を利用 何度MoveNextAsyncを呼んでも同じインスタンスを使いまわし
  61. 61. 1度に1人だけがawaitする想定 • continuation (OnCompleteで呼ぶ処理)の管理がシンプルに (=高速) • 複数いると… • List<Action>なフィールドを持って • lockを書けてAdd/Remove • 1度に1人だけだと • Actionなフィールドを持って • CompareExchangeで入れ替えするだけ List自体のnewが発生 lockと比べると軽い処理 ManualResetValueTaskSourceCore<T> はこっちを採用
  62. 62. 「1度に1人だけ」想定 • 普通は書かないようなコードを書くと例外が出る async IAsyncEnumerable<int> A() { await Task.Delay(10); yield return 1; await Task.Delay(10); yield return 2; } async Task B() { var a = A(); var e = a.GetAsyncEnumerator(); var t1 = e.MoveNextAsync(); var t2 = e.MoveNextAsync(); // t1 完了前に次の MoveNextAsync を呼ぶ await t1; // ここで例外が起こる } パフォーマンスとの引き換えで 意図的にそういう仕様
  63. 63. まとめ • 非同期で複数のデータを受け渡し • C# 8.0の新機能 • 非同期メソッドの拡張 : awaitとyieldの混在 • 非同期foreach : await foreach (var x in asyncStream) • パフォーマンスへの配慮 • ValueTask • 本当に非同期の時だけアロケーション • IValueTaskSource/ManualResetValueTaskSourceCore • 「1度に1人だけawait」な前提で最適化

×
Save this presentation