Ir a aprender idiomas: Ir al modelo de memoria [debe ver]

Enlace original

Introducción

El modelo de memoria Go estipula algunas condiciones. Bajo estas condiciones, el valor devuelto al leer una variable en una goroutine puede asegurar que es el valor escrito en la variable en otra goroutine. [Me tomó 3 horas y media traducir este artículo]

Ocurre antes (Ocurre antes)

En una goroutine, las operaciones de lectura y escritura deben comportarse como si fueran ejecutadas en el orden especificado en el programa. Esto se debe a que el compilador y el procesador en una goroutine pueden reorganizar el orden de ejecución de las operaciones de lectura y escritura (siempre que dicha ejecución fuera de orden no cambie el comportamiento definido en la especificación del lenguaje en la goroutine) .

Debido a la ejecución fuera de orden, el orden de ejecución observado por una gorutina puede ser diferente del orden de ejecución observado por otra gorutina . Por ejemplo, si se ejecuta una goroutine a = 1; b = 2;, otra goroutine puede observar que el valor de b se actualiza antes que a.

Para especificar las condiciones necesarias para la lectura y escritura, definimos ocurre antes , una secuencia parcial de operaciones de memoria realizadas en un programa Go. Si el evento ocurre en el evento e1 e2 antes , entonces decimos que e1 e2 ocurre después . De manera similar, si e1 no ocurre antes de e2 ni después de e2, entonces decimos que e1 y e2 ocurren simultáneamente .

En una sola goroutine, el orden de ocurre antes es el orden en el programa.

Se puede permitir que una operación de lectura r para la variable v observe una operación de escritura w av, si se cumplen las siguientes condiciones al mismo tiempo:

  1. r no ocurre antes de w
  2. Después de wy antes de r, no se producen otras operaciones de escritura antes de v.

Para asegurar que una operación de lectura r en la variable v observe una operación de escritura w en v, debe asegurarse que w sea la única operación de escritura permitida por r . Esto significa que se deben cumplir las siguientes condiciones al mismo tiempo:

  1. w ocurre antes de r
  2. Cualquier otra operación de escritura en la variable compartida v ocurre antes o después de r.

Estas dos condiciones son más estrictas que las dos condiciones anteriores y requieren que no se produzcan otras operaciones de escritura simultáneamente con w o r.

En una sola gorutina, no hay simultaneidad, por lo que estas dos definiciones son equivalentes: una operación de lectura r observa la operación de escritura más reciente de w a v. Cuando múltiples goroutines acceden a una variable compartida v, deben usar eventos sincronizados para establecer condiciones de suceso antes de asegurar que la operación de lectura observe la operación de escritura esperada.

En el modelo de memoria, el comportamiento de inicializar una variable con cero es el mismo que el comportamiento de una operación de escritura.

El comportamiento de leer y escribir un valor que excede el tamaño de una sola palabra de máquina [32 bits o 64 bits] es el mismo que el comportamiento de múltiples operaciones desordenadas en una sola palabra de máquina.

Sincronizar

inicialización

La operación de inicialización del programa se ejecuta en una sola goroutine, pero esta goroutine puede crear otras gorutinas que se ejecutan simultáneamente.

Si el paquete p importa el paquete q, entonces la finalización de la función init de q ocurre antes de la ejecución de cualquier función init de p.

La ejecución de la función main.main [es decir, la función principal] ocurre después de que se completan todas las funciones de inicio.

Creación de gorutina

La ejecución de la instrucción go que inicia una nueva goroutine ocurre antes de que la goroutine comience a ejecutarse.

Por ejemplo, en este programa:

var a string

func f() {
    
    
	print(a) // 后
}

func hello() {
    
    
	a = "hello, world"
	go f() // 先
}

Llamar a la función de saludo imprimirá "hola, mundo" en un punto de evento posterior. [Debido a que una instrucción = "hola, mundo" se ejecuta antes de la instrucción go f () y la función f ejecutada por goroutine se ejecuta después de la instrucción go f (), el valor de a se ha inicializado]

Destrucción goroutine

No se garantiza que la salida de la goroutine ocurra antes de cualquier evento del programa. Por ejemplo, en este programa:

var a string

func hello() {
    
    
	go func() {
    
     a = "hello" }()
	print(a)
}

La asignación de a no va seguida de ningún evento de sincronización, por lo que no hay garantía de que otras goroutines puedan observar la operación de asignación. De hecho, un compilador agresivo puede eliminar toda la instrucción go.

Si el efecto de la asignación en una goroutine debe ser observado por otra goroutine, utilice mecanismos de sincronización como cerraduras o comunicación por tubería para establecer un orden relativo.

Comunicación de canalización

La comunicación por canalización es el principal método de sincronización entre gorutinas. La operación de envío de una tubería coincide [correspondiente] a la operación de recepción de una tubería (generalmente en otra goroutine).

Una operación de envío en una tubería almacenada en búfer ocurre antes de que se complete la operación de recepción correspondiente.

Este programa:

var c = make(chan int, 10) // 有缓冲的管道
var a string

func f() {
    
    
	a = "hello, world"
	c <- 0 // 发送操作,先
}

func main() {
    
    
	go f()
	<-c // 接收操作,后
	print(a)
}

Capaz de asegurarse de que la salida "hola, mundo". Porque la operación de asignación a a se completa antes de la operación de envío y la operación de recepción se completa después de la operación de envío.

El cierre de una tubería ocurre antes de recibir un valor cero de la tubería.

En el ejemplo anterior, la c <- 0declaración se reemplaza el close(c)efecto es el mismo.

Se produce una operación de recepción en una tubería sin búfer antes de que se complete la operación de envío correspondiente.

Este programa (igual que el anterior, usando una tubería sin búfer, intercambiando las operaciones de envío y recepción):

var c = make(chan int) // 无缓冲的管道
var a string

func f() {
    
    
	a = "hello, world"
	<-c // 接收操作,先
}

func main() {
    
    
	go f()
	c <- 0 // 发送操作,后
	print(a)
}

También se asegurará de generar "hola, mundo".

Si la canalización está almacenada en búfer (por ejemplo c = make(chan int, 1)) , entonces el programa no puede garantizar la salida "hello, world"(puede imprimir una cadena vacía, bloquearse o hacer otras cosas).

La k-ésima operación de recepción en una tubería de capacidad C ocurre antes de que se complete la k + C-ésima operación de envío.

Esta regla generaliza la regla anterior a las tuberías con búfer. Permite el uso de una tubería en búfer para implementar un modelo de semáforo de conteo : el número de elementos en la tubería corresponde al número que se está utilizando [recuento de semáforos], y la capacidad de la tubería corresponde al número máximo de uso simultáneo. Enviando un elemento para obtener el semáforo, Recibe un elemento para liberar el semáforo. Este es un uso común para limitar la simultaneidad.

El siguiente programa para cada uno inicia una lista de procesamiento goroutine, pero el uso de limitla canalización para garantizar al mismo tiempo solo tres funciones de trabajo en tiempo de ejecución.

var limit = make(chan int, 3)

func main() {
    
    
	for _, w := range work {
    
    
		go func(w func()) {
    
    
			limit <- 1 // 获取信号量
			w()
			<-limit // 释放信号量
		}(w)
	}
	select{
    
    }
}

bloquear

syncEl paquete implementa dos tipos de datos de bloqueo sync.Mutexy sync.RWMutex.

Cualquier sync.Mutexo sync.RWMutextipo de variable ly n < m , la n-ésima l.Unlock()operación en el m-ésimo l.Lock()lugar antes de que regrese la operación.

Este programa:

var l sync.Mutex
var a string

func f() {
    
    
	a = "hello, world"
	l.Unlock() // 第一个 Unlock 操作,先
}

func main() {
    
    
	l.Lock()
	go f()
	l.Lock() // 第二个 Lock 操作,后
	print(a)
}

Garantizado para imprimir "hello, world".

Una vez

syncAl proporcionar el Oncetipo de paquete , se proporciona un mecanismo seguro para inicializar la pluralidad de rutinas. Se pueden ejecutar varios subprocesos una vez. Haga (f) una vez para una f en particular, pero solo uno ejecutará f (), y otras llamadas se bloquearán hasta que f () regrese.

De una once.Do(f)llamada f()se devuelve en cualquier once.Do(f)ocurrencia antes de regresar.

En este programa:

var a string
var once sync.Once

func setup() {
    
    
	a = "hello, world" // 先
}

func doprint() {
    
    
	once.Do(setup)
	print(a) // 后
}

func twoprint() {
    
    
	go doprint()
	go doprint()
}

Llamar a twoprint solo llamará al setup una vez. La función de configuración se completa antes de llamar a la función de impresión. El resultado se imprimirá dos veces "hola, mundo".

Sincronización incorrecta

Tenga en cuenta que una operación de lectura r puede observar el valor escrito por la operación de escritura w que ocurrió simultáneamente con ella. Cuando esto sucede, no hay garantía de que las operaciones de lectura que ocurren después de r puedan observar las operaciones de escritura que ocurren antes de w .

En este programa:

var a, b int

func f() {
    
    
	a = 1
	b = 2
}

func g() {
    
    
	print(b)
	print(a)
}

func main() {
    
    
	go f()
	g()
}

Puede suceder que la función g produzca 2 y luego 0. [El valor de b sale como 2, lo que indica que se ha observado la operación de escritura de b. Sin embargo, el valor de a es 0 después de leerse, lo que indica que no se observa la operación de escritura de a antes de la escritura de b. No se puede suponer que el valor de b es 2, entonces el valor de a debe ser 1.

Este hecho invalida alguna lógica de procesamiento común.

Por ejemplo, para evitar la sobrecarga causada por el bloqueo, el programa de dos impresiones puede estar escrito incorrectamente como:

var a string
var done bool

func setup() {
    
    
	a = "hello, world"
	done = true
}

func doprint() {
    
    
	if !done {
    
     // 不正确!
		once.Do(setup)
	}
	print(a)
}

func twoprint() {
    
    
	go doprint()
	go doprint()
}

Tal escritura no garantiza que la escritura hecha se observe en el doprint. Esta versión puede generar cadenas vacías incorrectamente.

Otra lógica de código incorrecta es hacer un bucle y esperar a que cambie un valor:

var a string
var done bool

func setup() {
    
    
	a = "hello, world"
	done = true
}

func main() {
    
    
	go setup()
	for !done {
    
     // 不正确!
	}
	print(a)
}

Como antes, en main, observar la escritura a hecha no significa que se observe la escritura a, por lo que este programa también puede imprimir una cadena vacía. Peor aún, no hay garantía de que las escrituras hechas sean observadas por main porque no hay un evento de sincronización entre los dos subprocesos. No se puede garantizar que el bucle principal se complete.

Un procedimiento similar es el siguiente:

type T struct {
    
    
	msg string
}

var g *T

func setup() {
    
    
	t := new(T)
	t.msg = "hello, world"
	g = t
}

func main() {
    
    
	go setup()
	for g == nil {
    
     // 不正确
	}
	print(g.msg)
}

Incluso si main observa g! = Nil y sale del bucle, no hay garantía de que haya observado el valor inicial de g.msg.

En todos estos ejemplos, la solución es la misma: use sincronización explícita .

Supongo que te gusta

Origin blog.csdn.net/woay2008/article/details/110748677
Recomendado
Clasificación