Este es un extracto de la publicación; la publicación completa está disponible aquí: https://victoriametrics.com/blog/go-sync-pool/
Esta publicación es parte de una serie sobre el manejo de la concurrencia en Go:
En el código fuente de VictoriaMetrics, usamos mucho sync.Pool y, sinceramente, se adapta perfectamente a la forma en que manejamos objetos temporales, especialmente búferes de bytes o divisiones.
Se usa comúnmente en la biblioteca estándar. Por ejemplo, en el paquete 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{} }
En este caso, sync.Pool se utiliza para reutilizar objetos *encodeState, que manejan el proceso de codificación JSON en un bytes.Buffer.
En lugar de simplemente tirar estos objetos después de cada uso, lo que solo le daría más trabajo al recolector de basura, los guardamos en un grupo (sync.Pool). La próxima vez que necesitemos algo similar, simplemente lo tomamos del grupo en lugar de crear uno nuevo desde cero.
También encontrará varias instancias de sync.Pool en el paquete net/http, que se utilizan para optimizar las operaciones de E/S:
package http var ( bufioReaderPool sync.Pool bufioWriter2kPool sync.Pool bufioWriter4kPool sync.Pool )
Cuando el servidor lee los cuerpos de las solicitudes o escribe respuestas, puede extraer rápidamente un lector o escritor preasignado de estos grupos, omitiendo asignaciones adicionales. Además, los 2 grupos de escritores, *bufioWriter2kPool y *bufioWriter4kPool, están configurados para manejar diferentes necesidades de escritura.
func bufioWriterPool(size int) *sync.Pool { switch size { case 2Muy bien, ya basta de introducción.
Hoy profundizaremos en de qué se trata sync.Pool, su definición, cómo se usa, qué sucede bajo el capó y todo lo que quizás quieras saber.
Por cierto, si quieres algo más práctico, hay un buen artículo de nuestros expertos en Go que muestra cómo usamos sync.Pool en VictoriaMetrics: Técnicas de optimización del rendimiento en bases de datos de series temporales: sync.Pool para operaciones vinculadas a CPU
¿Qué es sync.Pool?
En pocas palabras, sync.Pool in Go es un lugar donde puedes guardar objetos temporales para reutilizarlos más adelante.
Pero aquí está la cuestión: no controlas cuántos objetos permanecen en la piscina, y cualquier cosa que coloques allí se puede retirar en cualquier momento, sin previo aviso y sabrás por qué cuando leas la última sección.
Lo bueno es que el grupo está diseñado para ser seguro para subprocesos, por lo que varias gorutinas pueden acceder a él simultáneamente. No es una gran sorpresa, considerando que es parte del paquete de sincronización.
"¿Pero por qué nos molestamos en reutilizar objetos?"
Cuando tienes muchas rutinas ejecutándose a la vez, a menudo necesitan objetos similares. Imagínese ejecutar go f() varias veces al mismo tiempo.
Si cada gorutina crea sus propios objetos, el uso de memoria puede aumentar rápidamente y esto ejerce presión sobre el recolector de basura porque tiene que limpiar todos esos objetos una vez que ya no son necesarios.
Esta situación crea un ciclo en el que la alta concurrencia conduce a un uso elevado de la memoria, lo que luego ralentiza el recolector de basura. sync.Pool está diseñado para ayudar a romper este ciclo.
type Object struct { Data []byte } var pool sync.Pool = sync.Pool{ New: func() any { return &Object{ Data: make([]byte, 0, 1024), } }, }Para crear un grupo, puede proporcionar una función New() que devuelva un nuevo objeto cuando el grupo esté vacío. Esta función es opcional; si no la proporciona, el grupo simplemente devuelve cero si está vacío.
En el fragmento anterior, el objetivo es reutilizar la instancia de la estructura Object, específicamente el segmento que contiene.
Reutilizar la porción ayuda a reducir el crecimiento innecesario.
Por ejemplo, si el segmento crece a 8192 bytes durante el uso, puede restablecer su longitud a cero antes de volver a colocarlo en el grupo. La matriz subyacente todavía tiene una capacidad de 8192, por lo que la próxima vez que la necesite, esos 8192 bytes estarán listos para ser reutilizados.
func (o *Object) Reset() { o.Data = o.Data[:0] } func main() { testObject := pool.Get().(*Object) // do something with testObject testObject.Reset() pool.Put(testObject) }El flujo es bastante claro: obtienes un objeto del grupo, lo usas, lo reinicias y luego lo vuelves a colocar en el grupo. Se puede restablecer el objeto antes de devolverlo o inmediatamente después de sacarlo del grupo, pero no es obligatorio, es una práctica común.
Si no eres fanático del uso de afirmaciones de tipo pool.Get().(*Object), hay un par de formas de evitarlo:
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() }, }, } }
El contenedor genérico le brinda una forma más segura de trabajar con el grupo, evitando aserciones de tipo.
Solo tenga en cuenta que agrega un poco de sobrecarga debido a la capa adicional de dirección indirecta. En la mayoría de los casos, esta sobrecarga es mínima, pero si estás en un entorno altamente sensible a la CPU, es una buena idea ejecutar pruebas comparativas para ver si vale la pena.
Pero espera, hay más.
Si has notado en muchos ejemplos anteriores, incluidos los de la biblioteca estándar, lo que almacenamos en el grupo normalmente no es el objeto en sí, sino un puntero al objeto.
Déjame explicarte por qué con un ejemplo:
var pool = sync.Pool{ New: func() any { return []byte{} }, } func main() { bytes := pool.Get().([]byte) // do something with bytes _ = bytes pool.Put(bytes) }
Estamos usando un grupo de []bytes. Generalmente (aunque no siempre), cuando pasa un valor a una interfaz, es posible que el valor se coloque en el montón. Esto también sucede aquí, no solo con los cortes sino con cualquier cosa que pases a pool.Put() que no sea un puntero.
Si verifica utilizando el análisis de escape:
// escape analysis $ go build -gcflags=-m bytes escapes to heap
Ahora, no digo que nuestra variable bytes se mueva al montón, diría "el valor de los bytes se escapa al montón a través de la interfaz".
Para entender realmente por qué sucede esto, necesitaríamos profundizar en cómo funciona el análisis de escape (lo cual podríamos hacer en otro artículo). Sin embargo, si pasamos un puntero a pool.Put(), no hay asignación adicional:
var pool = sync.Pool{ New: func() any { return new([]byte) }, } func main() { bytes := pool.Get().(*[]byte) // do something with bytes _ = bytes pool.Put(bytes) }
Ejecute el análisis de escape nuevamente y verá que ya no hay escapes al montón. Si quieres saber más, hay un ejemplo en el código fuente de Go.
Antes de analizar cómo funciona realmente sync.Pool, vale la pena comprender los conceptos básicos del modelo de programación PMG de Go; esta es realmente la columna vertebral de por qué sync.Pool es tan eficiente.
Hay un buen artículo que desglosa el modelo PMG con algunas imágenes: Modelos PMG en Go
Si hoy te sientes perezoso y buscas un resumen simplificado, te cubro las espaldas:
PMG significa P (pprocesadores lógicos), M (mhilos de máquina) y G (gorrutinas). El punto clave es que cada procesador lógico (P) solo puede tener un subproceso de máquina (M) ejecutándose en cualquier momento. Y para que se ejecute una rutina (G), debe estar adjunta a un hilo (M).
Esto se reduce a 2 puntos clave:
Pero la cuestión es que una sincronización. El grupo en Go no es solo un grupo grande, en realidad está compuesto por varios grupos 'locales', cada uno de los cuales está vinculado a un contexto de procesador específico, o P, en el que se ejecuta el tiempo de ejecución de Go. gestionando en cualquier momento dado.
Cuando una gorutina que se ejecuta en un procesador (P) necesita un objeto del grupo, primero verificará su propio grupo P-local antes de buscar en otro lugar.
La publicación completa está disponible aquí: https://victoriametrics.com/blog/go-sync-pool/
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3