La esencia de la entrevista en idioma GO: ¿qué pasó antes con el canal? ¿Cuáles son las aplicaciones de los canales?

Definición de Wikipedia:

En informática, la relación sucedió antes (denominada: ->) es una relación entre el resultado de dos eventos, de modo que si un evento sucede antes que otro, el resultado debe reflejar eso, incluso si esos eventos en realidad se ejecutan. fuera de servicio (generalmente para optimizar el flujo del programa).

En pocas palabras, si existe una relación de sucedido antes entre el evento a y el evento b, es decir, a -> b, entonces los resultados después de completar a y b deben reflejar esta relación. Dado que los compiladores y CPU modernos realizarán varias optimizaciones, incluida la reorganización del compilador, la reorganización de la memoria, etc., las restricciones de lo ocurrido antes son muy importantes en el código concurrente.

Según lo que compartió el maestro Huang Yuepan sobre programación concurrente en Gopher China 2019, la relación de lo ocurrido antes entre el envío del canal, el envío finalizado, la recepción y la recepción finalizada es la siguiente:

  1. El enésimo senddebe ser happened beforeel enésimo receive finished, ya sea un canal con o sin búfer.
  2. Para un canal almacenado en búfer con una capacidad de m, el enésimo canal receivedebe ser happened beforeel n+mésimo canal send finished.
  3. Para canales sin búfer, el enésimo receivedebe ser happened beforeel enésimo send finished.
  4. El cierre del canal debe happened beforeser notificado por el receptor.

Expliquemoslo pieza por pieza.

El primer punto es que tenemos razón desde la perspectiva del código fuente. Enviar no es necesariamente happened beforerecibir, porque a veces se recibe primero, luego se suspende la rutina y luego el remitente la activa. El envío ocurrió después de la recepción. Pero pase lo que pase, si quieres completar la recepción, primero debes enviarla.

El segundo es un canal almacenado en búfer. Cuando ocurre el envío n + m, hay dos situaciones:

Si no se produce la enésima recepción. En este momento, el canal está lleno y el envío se bloqueará. Luego, cuando se produzca la enésima recepción, la rutina del remitente se activará y luego continuará con el proceso de envío. De esta forma, el enésimo receivedebe ser happened beforeel n+mésimo send finished.

Si ya se ha producido la enésima recepción, esto cumple directamente los requisitos.

El tercer artículo también es relativamente fácil de entender. Si el enésimo envío está bloqueado, la rutina del remitente se bloquea y la enésima recepción llega en este momento, antes de que finalice el enésimo envío. Si el enésimo envío no está bloqueado, significa que la enésima recepción ha estado esperando allí durante mucho tiempo. No solo sucedió antes de que finalizara el envío, sino que también sucedió antes del envío.

Artículo 4, recupere el código fuente, primero establezca cerrado = 1, luego active el receptor en espera y copie el valor cero en el receptor.

Materiales de referencia [Compartir programación concurrente de Bird's Nest] Hay un enlace de descarga al PPT en el área de comentarios de esta publicación de blog. Este es el discurso del Maestro Chao en la conferencia Gopher 2019.

Con respecto a lo sucedido antes, aquí hay otro ejemplo mencionado en el nuevo libro "Programación avanzada en lenguaje Go" de Chai Da y Cao Da.

La sección 1.5 del libro habla primero sobre el modelo de memoria de consistencia secuencial, que es la base de la programación concurrente.

Veamos directamente el ejemplo:

var done = make(chan bool)
var msg string

func aGoroutine() {
	msg = "hello, world"
	done <- true
}

func main() {
	go aGoroutine()
	<-done
	println(msg)
}

Primero defina un canal terminado y una cadena a imprimir. En la función principal, inicie una rutina, espere a que se reciba un valor de listo y luego ejecute la operación de imprimir mensaje. Si no existe <-donedicha línea de código en la función principal, el mensaje impreso estará vacío, porque una Gorrutina no tiene tiempo para programarse y asignar un valor al mensaje, y el programa principal se cerrará. En el lenguaje Go, la rutina principal no esperará a otras rutinas al salir.

Después de agregar <-doneesta línea de código, se bloqueará aquí. Después de que aGoroutine envíe un valor para finalizar, se activará y continuará imprimiendo el mensaje. Antes de esto, a msg se le había asignado un valor, por lo que se imprimirá hello, world.

El suceso anterior del que dependemos aquí es el primero mencionado anteriormente. El primer envío debe haber ocurrido antes de que finalice la primera recepción, es decir, ocurre done <- trueantes <-done, lo que significa que cuando se ejecuta la función principal y <-doneluego println(msg)se ejecuta esta línea de código, a msg ya se le ha asignado un valor, por lo que el resultado deseado será impreso. .

Aproveche aún más la regla del tercer suceso anterior mencionada anteriormente y modifique el código:

var done = make(chan bool)
var msg string

func aGoroutine() {
	msg = "hello, world"
	<-done
}

func main() {
	go aGoroutine()
	done <- true
	println(msg)
}

Se puede obtener el mismo resultado, ¿por qué? Según la tercera regla, para canales sin buffer, la primera recepción debe ocurrir antes de que finalice el primer envío. En otras palabras, ya sucedió antes de que se complete, lo que significa que al mensaje se le ha asignado un valor y eventualmente se
imprimirá .done <- true<-donehello, world

El canal puede causar fugas de rutina.

El motivo de la fuga es que después de que la rutina opera el canal, se encuentra en un estado de bloqueo de envío o recepción, mientras que el canal está en un estado lleno o vacío y no se puede cambiar. Al mismo tiempo, el recolector de basura no reciclará dichos recursos, lo que hará que la rutina permanezca en la cola de espera y nunca vea la luz del día.

Además, durante la ejecución del programa, si ninguna rutina hace referencia a un canal, gc lo reciclará sin causar pérdidas de memoria.

La combinación de Channel y goroutine es excelente para la programación concurrente de Go. Las aplicaciones prácticas de Channel suelen ser llamativas: al combinarse con seleccionar, cancelar, temporizador, etc., se pueden realizar varias funciones. A continuación, resolveremos la aplicación de canales.

señal de parada

La sección "Cómo cerrar canales correctamente" ya se ha discutido mucho, así que me saltaré esta sección.

Hay muchos escenarios en los que se utilizan canales para señales de parada. A menudo es para cerrar un canal o enviar un elemento al canal, de modo que la parte que recibe el canal pueda conocer esta información y luego realizar otras operaciones.

Cronograma de la tarea

Combinado con el temporizador, generalmente hay dos formas de jugar: implementar el control del tiempo de espera e implementar una determinada tarea con regularidad.

En ocasiones, si necesitas realizar una determinada operación pero no quieres que tarde demasiado, un temporizador puede hacerlo:

select {
	case <-time.After(100 * time.Millisecond):
	case <-s.stopc:
		return false
}

Después de esperar 100 ms, si s.stopc no ha leído los datos o está cerrado, finalizará directamente. Este es un ejemplo del código fuente de etcd. Esta forma de escribir se puede ver en todas partes.

Ejecutar una tarea con regularidad también es relativamente sencillo:

func worker() {
	ticker := time.Tick(1 * time.Second)
	for {
		select {
		case <- ticker:
			// 执行定时任务
			fmt.Println("执行 1s 定时任务")
		}
	}
}

Cada segundo se ejecuta una tarea programada.

Desacoplar productores y consumidores

Cuando se inicia el servicio, se inician n trabajadores como un grupo de corrutinas de trabajo. Estas corrutinas funcionan en un for {}bucle infinito, consumiendo tareas de trabajo de un determinado canal y ejecutándolas:

func main() {
	taskCh := make(chan int, 100)
	go worker(taskCh)

    // 塞任务
	for i := 0; i < 10; i++ {
		taskCh <- i
	}

    // 等待 1 小时 
	select {
	case <-time.After(time.Hour):
	}
}

func worker(taskCh <-chan int) {
	const N = 5
	// 启动 5 个工作协程
	for i := 0; i < N; i++ {
		go func(id int) {
			for {
				task := <- taskCh
				fmt.Printf("finish task: %d by worker %d\n", task, id)
				time.Sleep(time.Second)
			}
		}(i)
	}
}

Las cinco rutinas de trabajo constantemente obtienen tareas de la cola de trabajos, y el productor solo necesita enviar tareas al canal, desacoplando al productor y al consumidor.

Salida del programa:

finish task: 1 by worker 4
finish task: 2 by worker 2
finish task: 4 by worker 3
finish task: 3 by worker 1
finish task: 0 by worker 0
finish task: 6 by worker 0
finish task: 8 by worker 3
finish task: 9 by worker 1
finish task: 7 by worker 4
finish task: 5 by worker 2

Controlar el número de concurrencias

A veces, es necesario ejecutar cientos de tareas con regularidad; por ejemplo, algunas tareas informáticas fuera de línea deben ejecutarse periódicamente por ciudad todos los días. Sin embargo, el número de concurrencia no puede ser demasiado alto, porque el proceso de ejecución de la tarea depende de algunos recursos de terceros, lo que limita la tasa de solicitudes. En este momento, el número de concurrencia se puede controlar a través del canal.

El siguiente ejemplo es de "Programación avanzada en lenguaje Go":

var limit = make(chan int, 3)

func main() {
    // …………
    for _, w := range work {
        go func() {
            limit <- 1
            w()
            <-limit
        }()
    }
    // …………
}

Construya un canal amortiguado con una capacidad de 3. Luego, recorra la lista de tareas e inicie una rutina para completar cada tarea. Para ejecutar realmente la tarea, la acción de acceder al tercero se completa en w(). Antes de ejecutar w(), primero debe obtener la "licencia" del límite. Después de obtener la licencia, puede ejecutar w(), y una vez completada la ejecución, la tarea es devolver la "licencia". Esto le permite controlar la cantidad de gorutinas que se ejecutan simultáneamente.

Aquí, limit <- 1se coloca dentro de func en lugar de afuera porque:

Si está en la capa externa, controla la cantidad de gorutinas en el sistema, lo que puede bloquear el bucle for y afectar la lógica empresarial.

De hecho, el límite no tiene nada que ver con la lógica, sino simplemente con el ajuste del rendimiento. La semántica de colocarlo en la capa interna y externa es diferente.

Otra cosa a tener en cuenta es que si w() entra en pánico, es posible que la "licencia" no se devuelva, por lo que debe utilizar aplazar para garantizarlo.

Supongo que te gusta

Origin blog.csdn.net/zy_dreamer/article/details/132795386
Recomendado
Clasificación