パイプライン - .NETの新ガイドラインIOのAPI(B)
System.IO.Pipelines:.NETの高性能IO
System.IO.Pipelines IOが統一され、抽象化、ファイル、COMポート、ネットワークなどで、焦点は、読み取り、書き込み、およびバッファゾーンに発信者のフォーカスをさせることです、入力出力IDuplexPipeの典型的なものです。
これは、2つのバッファを書き込み、読み込みとIOクラスの抽象ようです。
現在はまだプレビューで公式の地位を得るため、著者は、ソケットとNetworkStreamが達成使用 Pipelines.Sockets.Unofficialを
System.IO.Pipelinesの使用を説明するために、既存のSimplSocketsライブラリを改造するために使用され、このブログのエントリの作成者で、以前使用System.IO.Pipelines変換StackExchange.Redisで述べた両方の著者、。
商品コード(SimplPipelines、KestrelServer )
## SimplSockets説明
+単純な送信(SEND)、要求/応答処理(SendRecieve)を完了することができるおそれ+ APIを同期
+は、単純なフレームプロトコルカプセル化されたメッセージデータ提供
+用いバイト[]
+すべてのクライアントにサーバ缶ブロードキャストメッセージ
+ハートビート検出等
これは非常に典型的な伝統的なソケットライブラリです。
##変換の説明
###プログラムの数および選択プロセスでデータをバッファリングします
1.データの別個のコピーとしてコピーアウトバイト[]が、高コストの簡単な使用に(割当及びコピー)2 ReadOnlySequence <バイト>、ゼロコピー、高速制限付き。操作は、パイプアドバンス上で実行すると、データが回復されます。サーバ処理シナリオの厳密な制御の下で比較的高いの使用を意味する、使用することができる(データは、要求コンテキストを逃れられません)。
スキーム2の拡張として3、クラスライブラリにコードを処理するデータペイロード解析(処理ReadOnlySequence <バイト>)、解放だけで完全な解体のデータは、いくつかのカスタム構造体のマップ(地図)ビットを必要とするかもしれません。ここではそれはメモリ構造体に直接マップする必要がありますか?
4. <バイト>メモリは、データの戻しコピーにより取得ArrayPool <バイト> .Shareプールから大きな配列を返す必要があり、呼び出し側が、そのような高い要件、プールに復帰する必要があります。そして、メモリ<T> T []から取得し、高度かつ危険な操作です。安全でない、リスクがあります。(すべてではないメモリ<T>ベースのIS ON T [])
5.妥協、リターンメモリー<T>(スパン<T提供 >) の事を、そしてユーザーよう、利用者には明らかにいくつかの明確なAPIを使用します私たちは、戻り結果を処理する方法を知っています。例えば、IDisposableをは/これを使用して、廃棄()は、リソースプールに戻すために呼び出されます。
著者は、発信者が時間をかけてデータを保存することができますし、パイプラインの正常な動作を妨げることはありませんが、また良いの使用ArrayPoolすることができ、ユニバーサルメッセージングAPIの設計は、5プログラムがより合理的であると考えています。呼び出し側が使用して使用していない場合は大きな問題ではないだろうが、1として、プログラムの使用のように、いくつかの効率が低下します。
しかし、完全にオプションを検討する必要性は、このようなクライアントの使用など、あなたの実際のシーンは、StackExchange.Redisスキーム3であり、ときにデータの葉スキーム2使用が要求コンテキストを許可されていません。..
基本的に不可能後、プログラムを選択したら変更します。
2は、最高効率化プログラムのために、専門家の助言を** **使用refの構造体の著者によって提案されています。
スキーム5は、ここで選択され、処理プログラムの違いは、4メモリである<T>、著者が使用System.Buffers.IMemoryOwner <T>インターフェイス
パブリックインターフェイスIMemoryOwner <T>:IDisposableを { メモリ<T>メモリ{取得します。} }
以下は、時に廃棄ローンの配列を返し、実装コードで、複数の戻り(非常に悪い)を避けるため、スレッドの安全性を検討します。
プライベートシールクラスArrayPoolOwner <T>:IMemoryOwner <T> { プライベート読み取り専用int型の_length。 プライベートT [] _oversized。 内部ArrayPoolOwner(T []特大、int型の長さ){ _length =長さ。 _oversized =特大。 } パブリックメモリ<T>メモリ=>新しいメモリ<T>(のgetArray()、0、_length)。 プライベートT []のgetArray()=> Interlocked.CompareExchange(REF _oversized、NULL、NULL) ?? 新しい説明ObjectDisposedExceptionをスロー(ToStringメソッド()); 公共のボイドのDispose(){ VAR ARR = Interlocked.Exchange(REF _oversized、NULL); (!ARR = NULL)ArrayPool <T> .Shared.Return(ARR)の場合。 } }
あなたは廃棄メモリーが再び失敗します呼び出す場合、すなわち、使用して使用し、再使用しないでください。
** ArrayPool **のいくつかの説明
配列から+ ArrayPoolローンあなたが必要以上に大きく、あなたが指定したサイズに属しているが、限界に思える(あなたは以下指定されたサイズを超えることができません)、ArrayPoolを参照してください。ArrayPool <T> .Shared.Rent(int型minimumLength);
デフォルトの+配列の戻りが空ではないので、あなたのローンのアレイにゴミデータがあるかもしれません。復帰時に必要に空にする場合、使用ArrayPool <T> .Shared.Return(ARR、真);
配列から+ ArrayPoolローンあなたが必要以上に大きく、あなたが指定したサイズに属しているが、限界に思える(あなたは以下指定されたサイズを超えることができません)、ArrayPoolを参照してください。ArrayPool <T> .Shared.Rent(int型minimumLength);
デフォルトの+配列の戻りが空ではないので、あなたのローンのアレイにゴミデータがあるかもしれません。復帰時に必要に空にする場合、使用ArrayPool <T> .Shared.Return(ARR、真);
提案のいくつかはArrayPool:
増加IMemoryOwner <T> RentOwned(int型の長さ )、T [](minimumLength int型)家賃や融資時に配列を空に、リターンに空にするオプションの配列。
増加IMemoryOwner <T> RentOwned(int型の長さ )、T [](minimumLength int型)家賃や融資時に配列を空に、リターンに空にするオプションの配列。
ここでの考え方は、IMemoryOwner <T>転送により所有権を達成することである典型的な呼び出しに続きます
ボイドのdoSomething(IMemoryOwner <バイト>データ){ {(データ)を使用し // ...ここに他のもの... DoTheThing(data.Memory)。 } // ...ここでより多くの事... }
ArrayPoolを借りても、頻繁に割り当てメカニズムを避けます。
**著者の警告:**
操作を使用して、個別に撮影したdata.Memoryと+台無しにしないが、それ(比較のために、このエラー基礎を)行っているではないでしょう
作者がMemoryManagerを実装することができると信じて、+誰かが配列MemoryMarshal使用に出てきます< T>(ArrayPoolOwner <T>: MemoryManager <T>、MemoryManagerので<T>:IMemoryOwner <T>) .Memoryようにその.Span障害など。
----著者らはまた、非常に:)の(思いやり)もつれました。
操作を使用して、個別に撮影したdata.Memoryと+台無しにしないが、それ(比較のために、このエラー基礎を)行っているではないでしょう
作者がMemoryManagerを実装することができると信じて、+誰かが配列MemoryMarshal使用に出てきます< T>(ArrayPoolOwner <T>: MemoryManager <T>、MemoryManagerので<T>:IMemoryOwner <T>) .Memoryようにその.Span障害など。
----著者らはまた、非常に:)の(思いやり)もつれました。
使用ReadOnlySequence <T> ArrayPoolOwner充填(構成、インスタンス化)
パブリック静的IMemoryOwner <T>リース<T>(このReadOnlySequence <T>ソース) { (source.IsEmpty)は<T>()空戻った場合、 int型のlen =確認((int型)source.Length)。 VAR ARR = ArrayPool <T> .Shared.Rent(LEN); //貸し出し source.CopyTo(ARR)。 新しいArrayPoolOwner <T>を(ARR、LEN)を返す; //は、処分時に返さ }
###基本的なAPI
サーバとクライアントのコードは、いくつかのスレッドの安全機構を書き込む必要として、異なるが重複する領域の数を有し、基本クラスを共有することが可能であり、受信したデータを処理するために、いくつかのリードサイクルを必要とします。ベースクラスは、パイプとして(入力、出力2本のパイプを含む)IDuplexPipeを用います。
パブリック抽象クラスSimplPipeline:IDisposableを { プライベートIDuplexPipeの_pipe。 保護SimplPipeline(IDuplexPipe管) => _pipe =パイプ。 公共のボイドのDispose()=>閉じます(); 公共のボイドを閉じる(){/ * / *パイプを燃やします} }
まず、書き込みスレッドセーフにメカニズムの必要性とは、発信者を過剰に阻止されません。中(StackExchange.Redis v1のを含む)、元のメッセージキューSimplSocketsを処理するために使用されます。同期エンキューメッセージを送る将来の瞬間に、発信者、およびソケットに書き込まれたメッセージをデキュー。この方法で問題がある
+多くの可動部分があります
+や「パイプライン」ビットの繰り返しは、
+多くの可動部分があります
+や「パイプライン」ビットの繰り返しは、
パイプ自体は、キュー自体は、出力(書き込み、送信)バッファ、キューを追加管に直接データを書き込む必要はありませんが含まれています。キャンセル元キューのマイナー効果のみ、StackExchange.Redis v1の中でキュー完全優先順位付けプロセス(キュージャンプ)を使用し、著者は、彼らはそれを心配していないと述べました。
**書かれたAPIの設計
+必ずしも時間同期である
+呼び出し側が単にメモリデータ(ReadOnlyMember <バイト>)、またはAPIで書かれた後にクリーンアップする(IMemoryOwner <バイト>)の部分を通過することができます。
+読み、別途書き込み(応答を考慮していない)と仮定
戻り値は、パイプは、通常、同期しているのでValueTaskを使用して書かれている、フラッシュが非同期とすることができる場合にのみ、実行パイプライン(パイプラインにバックアップされていない限り、ほとんどの場合に、同期しています)。
+必ずしも時間同期である
+呼び出し側が単にメモリデータ(ReadOnlyMember <バイト>)、またはAPIで書かれた後にクリーンアップする(IMemoryOwner <バイト>)の部分を通過することができます。
+読み、別途書き込み(応答を考慮していない)と仮定
ValueTask WriteAsync(IMemoryOwner <バイト>ペイロード、int型のmessageId)//呼び出し側はもはや使用ペイロード非同期(async)保護された、私たちは、クリーンアップする必要があります { (ペイロード)を使用し { await WriteAsync(payload.Memory、messageIdです)。 } } 保護されたValueTask WriteAsync(ReadOnlyMemory <バイト>ペイロード、int型のmessageId); //呼び出し側のクリーニングサービス
メッセージを識別するメッセージID、応答メッセージに応答して、後続の処理のために、メッセージ・ヘッダーを書き込みます。
戻り値は、パイプは、通常、同期しているのでValueTaskを使用して書かれている、フラッシュが非同期とすることができる場合にのみ、実行パイプライン(パイプラインにバックアップされていない限り、ほとんどの場合に、同期しています)。
###書かれたとエラー
あなたは最初のシングルの書き込み動作、この不適切でロック、それがうまく非同期動作と調整されていないためことを確認する必要があります。別のスレッドになりますが、非同期であり、フォローアップ(継続)に部分的に生じる可能性がフラッシュを考えてみましょう。非同期SemaphoreSlimとここでは互換性があります。ここでガイドです:**一般に、アプリケーション・コードは読みやすくするために最適化する必要があります。ライブラリのコードでは、パフォーマンスを最適化する必要があります。**
以下は、テキスト機械翻訳がある
> あなたが同意することができるか、それに同意しないかもしれないが、これは私がコードを書いた一般的なガイドです。あなたの脳はその領域に注力し、奇数長さを使用するように、私はメンテナンス多くの場合、一人の人間の経験による「深い、必ずしも必要ではないが広い」であってもよいし、コードライブラリは、目的の単焦点を持っている傾向があり、意味します最適化されたコードが可能です。「広い必ずしも必要ではないが深い」(さまざまなライブラリに隠された深さ) -代わりに、アプリケーション・コードは、より多くのコンジット異なる概念が関与する傾向があります。アプリケーションコードは、典型的には、より複雑で予測不可能な相互作用を持っているので、焦点は、上の「明白な権利」を維持し、上にある必要があります。
基本的に、ここでの私のポイントは、彼らは私が経験とベンチマークの広い範囲から知っている本当に重要なので、私は、アプリケーションのコードの最適化にはない通常のフォーカスの多くを置く傾向があることです。Soが 。。私はあなたが私と一緒にこの旅に乗り出す願って、非常に奇妙なことに見える何かをしなければなりません。
「明らかに正しい」コード
プライベート読み取り専用SemaphoreSlim _singleWriter =新しいSemaphoreSlim(1); ValueTask WriteAsync非同期(async)保護(ReadOnlyMemory <バイト>ペイロード、int型のmessageId) { await _singleWriter.WaitAsync()。 試します { WriteFrameHeader(作家、payload.Length、messageIdです)。 await writer.WriteAsync(ペイロード)。 } 最後に { _singleWriter.Release()。 } }
何の問題もなくこのコードは、しかし、すべての部品が同期が完了している場合でも、それはおそらくない、すべてのサイトが非同期処理を必要-------余分なステート・マシンを意味しています。
二つの質問によって再構築
-ライトワンスかどうか全く競争がありませんか?(競合なし)
-フラッシュを同期するかどうか
二つの質問によって再構築
-ライトワンスかどうか全く競争がありませんか?(競合なし)
-フラッシュを同期するかどうか
復興、元WriteAsyncは新しいWriteAsyncを追加し、WriteAsyncSlowPathと改名しました
達成するために、「非常に奇妙に見える何か」の著者
保護されたValueTask WriteAsync(ReadOnlyMemory <バイト>ペイロード、int型のmessageId) { //巻き貝を取得しよう。ない場合は、非同期に切り替え //ライターがすでに占有され、非同期 (もし!_singleWriter.Wait(0)) リターンWriteAsyncSlowPath(ペイロード、messageIdです)。 ブールリリース= TRUE; 試します { WriteFrameHeader(作家、payload.Length、messageIdです)。 VAR書き込み= writer.WriteAsync(ペイロード)。 (write.IsCompletedSuccessfully)リターンデフォルトであれば、 リリース= falseは、 AwaitFlushAndRelease(書き込み)を返します。 } 最後に { (リリース)_singleWriter.Release()であれば、 } } 非同期ValueTask AwaitFlushAndRelease(ValueTask <FlushResult>フラッシュ) { {のawaitフラッシュをしてみてください。} 最後に{_singleWriter.Release()。} }
三箇所
1. _singleWriter.Wait(0)ライターがアイドル状態であることを意味し、誰が呼び出しされていない
2 write.IsCompletedSuccessfully平均ライター同期フラッシュ
法は補助ハンドル、非同期フラッシュをAwaitFlushAndRelease 3.
-------- -------------------------------------------------- ---------------------------
###プロトコルヘッダ処理
INT 2つのプロトコルヘッダ、小端部、第一の長さからなる、第二は、messageIdに、8バイトの合計です。空WriteFrameHeader(PipeWriterライター、int型の長さは、int型のmessageId) { VARスパン= writer.GetSpan(8)。 BinaryPrimitives.WriteInt32LittleEndian( スパン長さ)。 BinaryPrimitives.WriteInt32LittleEndian( span.Slice(4)、messageIdです)。 writer.Advance(8)。 }
###クライアントの送信パイプ
パブリッククラスSimplPipelineClient:SimplPipeline { 公共の非同期タスク<IMemoryOwner <バイト>> SendReceiveAsync(ReadOnlyMemory <バイト>メッセージ) { VaRのTCS =新しいTaskCompletionSource <IMemoryOwner <バイト>>(); int型のmessageId; ロック(_awaitingResponses) { イベントID = ++ _ nextMessageId。 イベントID = 1(messageIdです== 0)であれば、 _awaitingResponses.Add(messageIdです、TCS)。 } await WriteAsync(メッセージ、messageIdです)。 tcs.Taskのawaitリターン。 } 公共の非同期タスク<IMemoryOwner <バイト>> SendReceiveAsync(IMemoryOwner <バイト>メッセージ) { (メッセージ)を使用し { SendReceiveAsync(message.Memory)のawaitリターン。 } } }
- _awaitingResponses A辞書、メッセージ保存バー(のmessageIdです)メッセージに対する応答に更なる処理のために送られてきました。
###ループを受けます
タスクStartReceiveLoopAsync非同期(async)保護(CancellationToken cancellationToken =デフォルト) { 試します { しばらく(!cancellationToken.IsCancellationRequested) { VAR readResult =のawait reader.ReadAsync(cancellationToken)。 (readResult.IsCanceled)ブレークであれば、 VARバッファ= readResult.Buffer。 makingProgress = falseがありました。 一方、(TryParseFrame(REFバッファ、VARペイロードアウト)VaRのmessageIdですアウト) { makingProgressは真=。 await OnReceiveAsync(ペイロード、messageIdです)。 } reader.AdvanceTo(buffer.Start、buffer.End)。 (!makingProgress && readResult.IsCompleted)であればブレーク。 } {reader.Complete()試みます。}キャッチ{} } キャッチ(例外の例) { してみてください{reader.Complete(EX); }キャッチ{} } } 保護された抽象ValueTask OnReceiveAsync(ReadOnlySequence <バイト>ペイロード、int型のmessageId)。
送信者とシステム環境によって決定されるものではバッファを受け取ることになる何時間、遅延はので、ここですべての非同期処理だけで罰金によって、避けられません。
- TryParseFrameフレームフォーマットに従って等実際のデータ、IDを解析し、データをリードバッファ
OnRecieveAsyncデータ処理は、このような応答/応答処理のよう-
事前にスケジュール通知導管を読み取り、読み取ったデータをリーダについてすぐに- ;
- TryParseFrameフレームフォーマットに従って等実際のデータ、IDを解析し、データをリードバッファ
OnRecieveAsyncデータ処理は、このような応答/応答処理のよう-
事前にスケジュール通知導管を読み取り、読み取ったデータをリーダについてすぐに- ;
分析フレーム
プライベートBOOL TryParseFrame( REF ReadOnlySequence <バイト>入力、 アウトReadOnlySequence <バイト>ペイロード、int型のmessageIdアウト) { もし(input.Length <8) ヘッダー{//十分でないデータ ペイロード=デフォルト; イベントID =デフォルト; falseを返します。 } int型の長さ。 IF(input.First.Length> = 8) 最初のセグメントにおける{//すでに8バイト 長さ= ParseFrameHeader( messageIdに出input.First.Span、); } 他 {//はローカルスパンに8つのバイトをコピーします スパン<バイト>ローカル= stackallocバイト[8]。 input.Slice(0、8).CopyTo(ローカル)。 長さ= ParseFrameHeader( 地元、messageIdにアウト)。 } //私たちは、「長さ」のバイトを持っていますか? IF(input.Length <長さ+ 8) { ペイロード=デフォルト; falseを返します。 } // 成功! ペイロード= input.Slice(8、長さ)。 入力= input.Slice(payload.End)。 trueを返します。 }
最初の段落がinput.Firstあるようバッファは、リンクリストのように、作品によって連続、作品ではありません。
コードは、主な用途のいくつかを例示する単純で、
ヘルパーメソッド
OnReceiveAsync
コードは、主な用途のいくつかを例示する単純で、
ヘルパーメソッド
静的int型ParseFrameHeader( ReadOnlySpan <バイト>入力、int型のmessageIdアウト) { VAR長= BinaryPrimitives .ReadInt32LittleEndian(入力)。 イベントID = BinaryPrimitives .ReadInt32LittleEndian(input.Slice(4))。 長さを返します。 }
OnReceiveAsync
保護されたオーバーライドValueTask OnReceiveAsync( ReadOnlySequence <バイト>ペイロード、int型のmessageId) { (messageIdです!= 0)の場合 {//要求/応答 TaskCompletionSource <IMemoryOwner <バイト>> TCS。 ロック(_awaitingResponses) { (_awaitingResponses.TryGetValue(messageIdに、アウトTCS))の場合 { _awaitingResponses.Remove(messageIdです)。 } } ?TCS .TrySetResult(payload.Lease()); } 他 {//迷惑 ?MessageReceived .Invoke(payload.Lease()); } デフォルトを返します。 }
これまでのところ、残りは主に一部のサーバーや他の機能が実現し、ベンチマークします。。。