Sincronización de Go Diary-Secret of Go 1.9

转自https://colobu.com/2017/07/11/dive-into-sync-Map/

Antes de Go 1.6, el tipo de mapa integrado es parcialmente seguro para las rutinas, y no hay ningún problema con la lectura simultánea y puede haber problemas con la escritura simultánea. Desde la versión 1.6, la lectura y escritura simultáneas del mapa informará un error. Este problema existe en algunas bibliotecas de código abierto conocidas, por lo que la solución antes de la versión 1.9 es vincular un bloqueo adicional, encapsularlo en una nueva estructura o usar el bloqueo por separado Todo está bien.

Este artículo lo profundiza en la implementación específica de sync.Map y verá cómo el código se vuelve complicado para agregar una función, y las ideas del autor al implementar sync.Map.

Mapa con problemas de concurrencia

Las preguntas frecuentes oficiales han mencionado que el mapa integrado no es seguro para subprocesos (goroutine).

Primero, veamos un fragmento de código para lectura y escritura simultáneas. En el siguiente programa, una goroutine sigue leyendo y una goroutine escribe el mismo valor de clave. Es decir, incluso si las claves para leer y escribir no son las mismas, y el mapa no tiene operaciones como "expansión", el código Seguirá informando un error.

package main
func main() {
	m := make(map[int]int)
	go func() {
		for {
			_ = m[1]
		}
	}()
	go func() {
		for {
			m[2] = 2
		}
	}()
	select {}
}

El mensaje de error es: error fatal: lectura y escritura de mapa concurrentes.

Si observa el código fuente de Go: hashmap_fast.go # L118, verá que la marca hashWriting está marcada al leer, y si hay esta marca, se informará un error de concurrencia.

Esta bandera se establecerá al escribir: hashmap.go # L542

h.flags |= hashWriting

Después de establecer hashmap.go # L628, esta marca se cancelará.

Por supuesto, hay varias comprobaciones de lectura y escritura simultáneas en el código. Por ejemplo, al escribir, comprobará si hay escritura simultánea. Al eliminar claves, es similar a escribir. Al recorrer, hay problemas con la lectura y escritura simultáneas.

A veces, los problemas de simultaneidad de mapas no son tan fáciles de encontrar, puede usar el parámetro -race para verificar.

Solución antes de Go 1.9

Sin embargo, muchas veces usaremos objetos del mapa al mismo tiempo, especialmente en una cierta escala de proyectos, el mapa siempre guardará los datos compartidos por goroutines. Se proporciona una solución simple en el artículo Mapas de Go en acción en el blog oficial de Go.

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

Utiliza una estructura incrustada para agregar un bloqueo de lectura y escritura al mapa.

Es conveniente bloquear al leer datos:

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

Al escribir datos:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

sync.Map

Se puede decir que la solución anterior es bastante concisa, y el uso de bloqueos de lectura y escritura en lugar de Mutex puede reducir aún más el rendimiento de los bloqueos al leer y escribir.

Sin embargo, también tiene problemas en algunos escenarios. Si está familiarizado con Java, puede comparar la implementación de ConcurrentHashMap de Java. En el caso de datos de mapas muy grandes, un bloqueo hará que grandes clientes concurrentes compitan por un bloqueo. , La solución de Java es el fragmento, que utiliza múltiples bloqueos internamente, y cada intervalo comparte un bloqueo, lo que reduce el impacto en el rendimiento de compartir un bloqueo de datos. Orcaman proporciona una implementación de esta idea: concurrent-map, También pregunté a los desarrolladores relacionados con Go si deben implementar este esquema en Go. Debido a la complejidad de la implementación, la respuesta es Sí, lo consideramos. Sin embargo, a menos que haya mejoras especiales de rendimiento y escenarios de aplicación, no hay más novedades de desarrollo. .

Entonces, ¿cómo se implementa sync.Map en Go 1.9? ¿Cómo resuelve la concurrencia y mejora el rendimiento?

Hay varios puntos de optimización en la implementación de sync.Map, que se enumeran primero y los analizaremos más adelante.

Espacio para el tiempo. A través de dos estructuras de datos redundantes (leídas, sucias), se realiza el efecto del bloqueo en el rendimiento.
Utilice datos de solo lectura (lectura) para evitar conflictos de lectura y escritura.
Ajuste dinámico, después de más tiempos perdidos, los datos sucios se actualizan para leer.
doble revisión.
Eliminación retrasada. Eliminar un valor clave es solo una marca, y los datos eliminados solo se limpian cuando se promueve el sucio.
Se da prioridad a la lectura, actualización y eliminación de la lectura, porque la lectura de lectura no requiere un bloqueo.
A continuación, presentamos el código clave de sync.Map para comprender su idea de realización.

Primero, veamos la estructura de datos de sync.Map:

type Map struct {
	// 当涉及到dirty数据的操作的时候,需要使用这个锁
	mu Mutex
	// 一个只读的数据结构,因为只读,所以不会有读写冲突。
	// 所以从这个数据中读取总是安全的。
	// 实际上,实际也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。
	read atomic.Value // readOnly
	// dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),有些数据还可能没有移动到read字段中。
	// 对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
	// 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
	dirty map[interface{}]*entry
	// 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
	// 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
	misses int
}

Su estructura de datos es muy simple, el valor contiene cuatro campos: read, mu, dirty, miss.

Utiliza estructuras de datos redundantes leídas y sucias. Dirty contendrá entradas que se eliminan en lectura y las entradas recién agregadas se agregarán a dirty.

La estructura de datos de lectura es:

type readOnly struct {
	m       map[interface{}]*entry
	amended bool // 如果Map.dirty有些数据不在中的时候,这个值为true
}

Enmendado indica que hay datos en Map.dirty que no están incluidos en readOnly.m, por lo que si no puede encontrar datos de Map.read, debe ir a Map.dirty para encontrarlos.

La modificación de Map.read se realiza mediante operaciones atómicas.

Aunque los datos leídos y sucios tienen datos redundantes, estos datos apuntan a los mismos datos a través de punteros, por lo que aunque el valor del mapa será grande, la ocupación de espacio redundante sigue siendo limitada.

El tipo de valor almacenado en readOnly.m y Map.dirty es * entrada, que contiene un puntero p que apunta al valor almacenado por el usuario.

type entry struct {
	p unsafe.Pointer // *interface{}
}

Hay tres valores para p:

nil: la entrada ha sido eliminada y m.dirty es nil
eliminada: la entrada ha sido eliminada y m.dirty no es nil y esta entrada no existe en m.dirty
Otro: la entrada es un valor normal
o más La estructura de datos de sync.Map, centrémonos en los cuatro métodos de Cargar, Almacenar, Eliminar y Rango. Otros métodos auxiliares pueden entenderse haciendo referencia a estos cuatro métodos.

Carga

El método de carga es proporcionar una clave, encontrar el valor correspondiente, si no existe, reflejar a través de ok:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	// 1.首先从m.read中得到只读readOnly,从它的map中查找,不需要加锁
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// 2. 如果没找到,并且m.dirty中有新数据,需要从m.dirty查找,这个时候需要加锁
	if !ok && read.amended {
		m.mu.Lock()
		// 双检查,避免加锁的时候m.dirty提升为m.read,这个时候m.read可能被替换了。
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		// 如果m.read中还是不存在,并且m.dirty中有新数据
		if !ok && read.amended {
			// 从m.dirty查找
			e, ok = m.dirty[key]
			// 不管m.dirty中存不存在,都将misses计数加一
			// missLocked()中满足条件后就会提升m.dirty
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok {
		return nil, false
	}
	return e.load()
}

Hay dos valores que preocupan aquí. Una es cargar desde m.read primero. Si no existe y hay nuevos datos en m.dirty, bloquéelo y luego cárguelo desde m.dirty.

La segunda es que aquí se usa el procesamiento de doble verificación, porque en las dos siguientes declaraciones, estas dos líneas de declaraciones no son una operación atómica.

if !ok && read.amended {
		m.mu.Lock()

Aunque la condición se cumple cuando se ejecuta la primera oración, m.dirty se puede promover a m.read antes del bloqueo, por lo que se debe verificar m.read después del bloqueo. Este método se utiliza en los métodos posteriores.

La tecnología de doble verificación es muy familiar para los programadores de Java.Una de las formas de realización del modo singleton es utilizar la tecnología de doble verificación.

Se puede ver que si el valor de la clave que consultamos existe en m.read, no hay necesidad de bloquear y regresar directamente, lo cual es teóricamente excelente. Incluso si no existe en m.read, después de algunos errores, m.dirty se promoverá a m.read y se buscará desde m.read. Por lo tanto, para los casos en los que hay pocas actualizaciones / adiciones y se cargan muchas claves, el rendimiento es básicamente similar al de un mapa sin bloqueo.

Veamos cómo se promovió m.dirty. El método missLocked puede aumentar m.dirty.

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

Las últimas tres líneas de código anteriores son para promover m.dirty, simplemente use m.dirty como el campo m de readOnly y actualice atómicamente m.read. Después de la actualización, m.dirty y m.misses se restablecen, y m.read.amended es falso.

Tienda

Este método es para actualizar o agregar una entrada.

func (m *Map) Store(key, value interface{}) {
	// 如果m.read存在这个键,并且这个entry没有被标记删除,尝试直接存储。
	// 因为m.dirty也指向这个entry,所以m.dirty也保持最新的entry。
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}
	// 如果`m.read`不存在或者已经被标记删除
	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() { //标记成未被删除
			m.dirty[key] = e //m.dirty中不存在这个键,所以加入m.dirty
		}
		e.storeLocked(&value) //更新
	} else if e, ok := m.dirty[key]; ok { // m.dirty存在这个键,更新
		e.storeLocked(&value)
	} else { //新键值
		if !read.amended { //m.dirty中没有新的数据,往m.dirty中增加第一个新键
			m.dirtyLocked() //从m.read中复制未删除的数据
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		m.dirty[key] = newEntry(value) //将这个entry加入到m.dirty中
	}
	m.mu.Unlock()
}
func (m *Map) dirtyLocked() {
	if m.dirty != nil {
		return
	}
	read, _ := m.read.Load().(readOnly)
	m.dirty = make(map[interface{}]*entry, len(read.m))
	for k, e := range read.m {
		if !e.tryExpungeLocked() {
			m.dirty[k] = e
		}
	}
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := atomic.LoadPointer(&e.p)
	for p == nil {
		// 将已经删除标记为nil的数据标记为expunged
		if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
	}
	return p == expunged
}

Como puede ver, las operaciones anteriores comienzan con la operación m.read primero, y luego agregue el candado si no se cumplen las condiciones, y luego opere m.dirty.

La Tienda puede copiar datos de m.read en determinadas circunstancias (inicialización o justo después de que se promueva m.dirty). Si la cantidad de datos en m.read es muy grande en este momento, el rendimiento puede verse afectado.

Eliminar

Eliminar un valor clave.

func (m *Map) Delete(key interface{}) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			delete(m.dirty, key)
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete()
	}
}

Del mismo modo, la operación de eliminación aún comienza desde m.read. Si la entrada no existe en m.read y hay nuevos datos en m.dirty, bloquee e intente eliminarla de m.dirty.

Tenga en cuenta que todavía tiene que comprobarlo. Simplemente elimínelo directamente de m.dirty, como si no existiera, pero si se elimina de m.read, no se eliminará directamente, sino que se marcará:

func (e *entry) delete() (hadValue bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		// 已标记为删除
		if p == nil || p == expunged {
			return false
		}
		// 原子操作,e.p标记为nil
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}

Rango

Debido a que for ... range map es una función de lenguaje incorporada, no hay forma de usar for range para atravesar sync.Map, pero puedes usar su método Range para atravesar devoluciones de llamada.

func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	// 如果m.dirty中有新数据,则提升m.dirty,然后在遍历
	if read.amended {
		//提升m.dirty
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly) //双检查
		if read.amended {
			read = readOnly{m: m.dirty}
			m.read.Store(read)
			m.dirty = nil
			m.misses = 0
		}
		m.mu.Unlock()
	}
	// 遍历, for range是安全的
	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

Se puede realizar una promoción de m.dirty antes de que se llame al método Range, pero la promoción de m.dirty no es una operación que requiera mucho tiempo.

Rendimiento del mapa sincronizado

Las pruebas de rendimiento se proporcionan en el código fuente de Go 1.9: map_bench_test.go, map_reference_test.go

También modifiqué el código en función de estos códigos y obtuve los siguientes datos de prueba. En comparación con la solución anterior, el rendimiento ha mejorado un poco. Si prestas especial atención al rendimiento, puedes considerar sync.Map.

BenchmarkHitAll/*sync.RWMutexMap-4   							20000000	        83.8 ns/op
BenchmarkHitAll/*sync.Map-4          							30000000	        59.9 ns/op
BenchmarkHitAll_WithoutPrompting/*sync.RWMutexMap-4         	20000000	        96.9 ns/op
BenchmarkHitAll_WithoutPrompting/*sync.Map-4                	20000000	        64.1 ns/op
BenchmarkHitNone/*sync.RWMutexMap-4                         	20000000	        79.1 ns/op
BenchmarkHitNone/*sync.Map-4                                	30000000	        43.3 ns/op
BenchmarkHit_WithoutPrompting/*sync.RWMutexMap-4            	20000000	        81.5 ns/op
BenchmarkHit_WithoutPrompting/*sync.Map-4                   	30000000	        44.0 ns/op
BenchmarkUpdate/*sync.RWMutexMap-4                          	 5000000	       328 ns/op
BenchmarkUpdate/*sync.Map-4                                 	10000000	       146 ns/op
BenchmarkUpdate_WithoutPrompting/*sync.RWMutexMap-4         	 5000000	       336 ns/op
BenchmarkUpdate_WithoutPrompting/*sync.Map-4                	 5000000	       324 ns/op
BenchmarkDelete/*sync.RWMutexMap-4                          	10000000	       155 ns/op
BenchmarkDelete/*sync.Map-4                                 	30000000	        55.0 ns/op
BenchmarkDelete_WithoutPrompting/*sync.RWMutexMap-4         	10000000	       173 ns/op
BenchmarkDelete_WithoutPrompting/*sync.Map-4                	10000000	       147 ns/op

otro

sync.Map no tiene un método Len, y actualmente no hay ninguna señal para agregarlo (problema # 20680 ), por lo que si desea obtener la cantidad de entradas válidas en el mapa actual, debe usar el método Range para recorrer una vez, lo cual es más doloroso.

Si existe la clave proporcionada, el método LoadOrStore devuelve el valor existente (Cargar); de lo contrario, guarda el valor de la clave proporcionada (Tienda).

Supongo que te gusta

Origin blog.csdn.net/qq_32198277/article/details/86579779
Recomendado
Clasificación