これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: https://victoriametrics.com/blog/go-sync-pool/
この投稿は、Go での同時実行性の処理に関するシリーズの一部です:
VictoriaMetrics ソース コードでは、sync.Pool を頻繁に使用します。正直に言って、一時オブジェクト、特にバイト バッファーやスライスの処理方法に非常に適しています。
標準ライブラリでよく使われます。たとえば、encoding/json パッケージでは:
package json var encodeStatePool sync.Pool // An encodeState encodes JSON into a bytes.Buffer. type encodeState struct { bytes.Buffer // accumulated output ptrLevel uint ptrSeen map[any]struct{} }
この場合、sync.Pool は、JSON を bytes.Buffer にエンコードするプロセスを処理する *encodeState オブジェクトを再利用するために使用されています。
これらのオブジェクトを使用のたびにスローするだけではガベージ コレクターの作業が増えるだけですが、代わりに、それらをプール (sync.Pool) に隠します。次回同様のものが必要になったときは、新しいものを最初から作成するのではなく、プールから取得するだけです。
net/http パッケージには、I/O 操作の最適化に使用される複数の sync.Pool インスタンスもあります:
package http var ( bufioReaderPool sync.Pool bufioWriter2kPool sync.Pool bufioWriter4kPool sync.Pool )
サーバーは、リクエスト本文を読み取るか応答を書き込むときに、事前に割り当てられたリーダーまたはライターをこれらのプールから迅速に取得し、追加の割り当てをスキップできます。さらに、2 つのライター プール *bufioWriter2kPool と *bufioWriter4kPool は、さまざまな書き込みニーズを処理するために設定されています。
func bufioWriterPool(size int) *sync.Pool { switch size { case 2さて、イントロはこれで十分です。
今日は、sync.Pool の概要、定義、使用方法、内部で何が起こっているのか、その他知りたいことすべてについて詳しく説明します。
ところで、もっと実用的なものが必要な場合は、VictoriaMetrics での sync.Pool の使用方法を示す Go 専門家による優れた記事があります: 時系列データベースのパフォーマンス最適化手法: CPU バウンド操作のための sync.Pool
同期プールとは何ですか?
簡単に言うと、Go の sync.Pool は、後で再利用できるように一時オブジェクトを保存できる場所です。
しかし、問題は、プールに残すオブジェクトの数を制御することはできず、そこに置いたものはいつでも、警告なしに削除される可能性があるということです。その理由は、最後のセクションを読むとわかります。
良い点は、プールがスレッドセーフになるように構築されているため、複数のゴルーチンが同時にプールを利用できることです。これが同期パッケージの一部であることを考えると、それほど驚くべきことではありません。
「でも、なぜわざわざオブジェクトを再利用するのでしょうか?」
一度に多くのゴルーチンを実行している場合、多くの場合、同様のオブジェクトが必要になります。 go f() を複数回同時に実行することを想像してください。
各ゴルーチンが独自のオブジェクトを作成すると、メモリ使用量が急速に増加する可能性があり、不要になったオブジェクトをすべてクリーンアップする必要があるため、ガベージ コレクターに負担がかかります。
この状況では、同時実行性が高いとメモリ使用量が多くなり、ガベージ コレクターの速度が低下するというサイクルが発生します。 sync.Pool は、このサイクルを打破するために設計されています。
type Object struct { Data []byte } var pool sync.Pool = sync.Pool{ New: func() any { return &Object{ Data: make([]byte, 0, 1024), } }, }プールを作成するには、プールが空のときに新しいオブジェクトを返す New() 関数を提供します。この関数はオプションです。指定しない場合、プールは空の場合に nil を返すだけです。
上記のスニペットの目的は、Object struct インスタンス、特にその内部のスライスを再利用することです。
スライスを再利用すると、不必要な成長を減らすことができます。
たとえば、使用中にスライスが 8192 バイトに増加した場合、プールに戻す前にその長さをゼロにリセットできます。基になる配列にはまだ 8192 の容量があるため、次回必要になったときに、これらの 8192 バイトを再利用できるようになります。
func (o *Object) Reset() { o.Data = o.Data[:0] } func main() { testObject := pool.Get().(*Object) // do something with testObject testObject.Reset() pool.Put(testObject) }フローは非常に明確です。プールからオブジェクトを取得し、それを使用し、リセットして、プールに戻します。オブジェクトのリセットは、オブジェクトを戻す前またはプールから取得した直後に行うことができますが、必須ではなく一般的な方法です。
型アサーション pool.Get().(*Object) の使用が好きではない場合は、それを回避する方法がいくつかあります。
func getObjectFromPool() *Object { obj := pool.Get().(*Object) return obj }
type Pool[T any] struct { sync.Pool } func (p *Pool[T]) Get() T { return p.Pool.Get().(T) } func (p *Pool[T]) Put(x T) { p.Pool.Put(x) } func NewPool[T any](newF func() T) *Pool[T] { return &Pool[T]{ Pool: sync.Pool{ New: func() interface{} { return newF() }, }, } }
汎用ラッパーを使用すると、型アサーションを回避して、プールを操作するためのより型安全な方法が得られます。
間接層が追加されるため、わずかなオーバーヘッドが追加されることに注意してください。ほとんどの場合、このオーバーヘッドは最小限ですが、CPU に非常に敏感な環境にいる場合は、ベンチマークを実行して、それだけの価値があるかどうかを確認することをお勧めします。
しかし、待ってください。それだけではありません。
標準ライブラリの例を含む、これまでの多くの例からお気づきかと思いますが、プールに保存するのは通常、オブジェクト自体ではなく、オブジェクトへのポインタです。
その理由を例を挙げて説明しましょう:
var pool = sync.Pool{ New: func() any { return []byte{} }, } func main() { bytes := pool.Get().([]byte) // do something with bytes _ = bytes pool.Put(bytes) }
[]バイトのプールを使用しています。一般に (常にではありませんが)、インターフェイスに値を渡すと、その値がヒープに配置されることがあります。これはここでも発生します。スライスだけでなく、ポインタではない pool.Put() に渡すものすべてで発生します。
エスケープ解析を使用してチェックする場合:
// escape analysis $ go build -gcflags=-m bytes escapes to heap
さて、変数バイトがヒープに移動するとは言いません。「バイトの値がインターフェイスを介してヒープにエスケープされる」と言いたいです。
なぜこれが起こるのかを本当に理解するには、エスケープ分析がどのように機能するかを詳しく調べる必要があります (これについては別の記事で行う可能性があります)。ただし、ポインタを pool.Put() に渡す場合、追加の割り当てはありません:
var pool = sync.Pool{ New: func() any { return new([]byte) }, } func main() { bytes := pool.Get().(*[]byte) // do something with bytes _ = bytes pool.Put(bytes) }
エスケープ分析を再度実行すると、ヒープへのエスケープがなくなっていることがわかります。さらに詳しく知りたい場合は、Go のソース コードに例があります。
sync.Pool が実際にどのように機能するかを説明する前に、Go の PMG スケジューリング モデルの基本を理解する価値があります。これが、sync.Pool が非常に効率的である理由のバックボーンです。
PMG モデルをいくつかのビジュアルで詳細に説明した優れた記事があります: Go の PMG モデル
今日は怠けていると感じていて、簡単な概要を探しているなら、私がサポートします:
PMG は、P (論理 p プロセッサ)、M (m マシン スレッド)、および G (goroutines) を表します。重要な点は、各論理プロセッサ (P) 上で実行できるマシン スレッド (M) は常に 1 つだけであるということです。また、ゴルーチン (G) を実行するには、スレッド (M) にアタッチする必要があります。
これは 2 つの重要なポイントに要約されます:
しかし実際には、Go の sync.Pool は単なる 1 つの大きなプールではなく、実際にはいくつかの「ローカル」プールで構成されており、それぞれが Go のランタイムである特定のプロセッサ コンテキスト (P) に関連付けられています。いつでも管理できます。
プロセッサ (P) 上で実行されているゴルーチンがプールからのオブジェクトを必要とする場合、他の場所を探す前に、まず自身の P ローカル プールをチェックします。
投稿全文はこちらからご覧いただけます: https://victoriametrics.com/blog/go-sync-pool/
免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。
Copyright© 2022 湘ICP备2022001581号-3