La esencia de la entrevista en idioma GO: ¿cómo cerrar el canal con elegancia?

En cuanto al uso de canales, existen varios inconvenientes:

  1. No hay forma de saber si un canal está cerrado sin cambiar el estado del canal.
  2. Cerrar un canal cerrado provocará pánico. Por lo tanto, es muy peligroso si la parte que cierra el canal lo cierra precipitadamente sin saber si está cerrado.
  3. Enviar datos a un canal cerrado provocará pánico. Por lo tanto, es muy peligroso si la parte que envía datos al canal no sabe si el canal está cerrado y luego envía datos al canal precipitadamente.

Una función aproximada para comprobar si el canal está cerrado:

func IsClosed(ch <-chan T) bool {
	select {
	case <-ch:
		return true
	default:
	}

	return false
}

func main() {
	c := make(chan T)
	fmt.Println(IsClosed(c)) // false
	close(c)
	fmt.Println(IsClosed(c)) // true
}

Al observar el código, en realidad hay muchos problemas. En primer lugar, la función IsClosed es una función con efectos secundarios. Cada vez que se llama, se leerá un elemento del canal y se cambiará el estado del canal. Esta no es una buena función, ¡simplemente haz el trabajo y aprovéchalo!

En segundo lugar, el resultado devuelto por la función IsClosed solo representa el momento en que se llama, y ​​no hay garantía de que otras gorutinas realicen algunas operaciones en él después de la llamada, cambiando su estado. Por ejemplo, si la función IsClosed devuelve verdadero, pero en este momento otra rutina cierra el canal y usted todavía mantiene esta información obsoleta de "el canal no está cerrado" y le envía datos, se producirá el pánico. Por supuesto, un canal no se cerrará dos veces, si el resultado devuelto por la función IsClosed es verdadero, significa que el canal está realmente cerrado.

Existe un principio ampliamente difundido para cerrar canales:

no cierre un canal desde el lado del receptor y no cierre un canal si el canal tiene varios remitentes simultáneos.

No cierre el canal desde el lado del receptor y no cierre el canal cuando haya varios remitentes.

Es más fácil entender que el remitente envía elementos al canal, por lo que el remitente puede decidir cuándo no enviar datos y cerrar el canal. Pero si hay varios remitentes, un remitente no puede determinar el estado de otros remitentes y el canal no se puede cerrar precipitadamente.

Pero lo dicho anteriormente no es lo más esencial. Sólo hay un principio muy esencial:

no cierre (ni envíe valores a) canales cerrados.

Hay dos formas menos elegantes de cerrar un canal:

  1. Utilice el mecanismo de aplazamiento-recuperación para cerrar el canal de forma segura o enviar datos al canal. Incluso si ocurre un pánico, la recuperación diferida está ahí para protegerlo.

  2. Utilice sync.Once para garantizar un solo apagado.

Entonces, ¿cómo deberíamos cerrar el canal con elegancia?

Según el número de remitentes y receptores, existen varias situaciones:

  1. Un remitente, un receptor
  2. Un remitente, M receptores
  3. N remitentes, un receptor
  4. N emisores, M receptores

Para 1 y 2, no hace falta decir que solo hay un remitente, simplemente ciérrelo directamente desde el lado del remitente, no hay problema. Concéntrese en los casos 3 y 4.

En el tercer caso, la forma de cerrar elegantemente el canal es: el único receptor dice “por favor deja de enviar más” cerrando un canal de señal adicional.

La solución es agregar un canal que transmita la señal de cierre y el receptor emite la instrucción de cerrar el canal de datos a través del canal de señal. Los remitentes dejan de enviar datos después de escuchar la señal de apagado. El código se muestra a continuación:

func main() {
	rand.Seed(time.Now().UnixNano())

	const Max = 100000
	const NumSenders = 1000

	dataCh := make(chan int, 100)
	stopCh := make(chan struct{})

	// senders
	for i := 0; i < NumSenders; i++ {
		go func() {
			for {
				select {
				case <- stopCh:
					return
				case dataCh <- rand.Intn(Max):
				}
			}
		}()
	}

	// the receiver
	go func() {
		for value := range dataCh {
			if value == Max-1 {
				fmt.Println("send stop signal to senders.")
				close(stopCh)
				return
			}

			fmt.Println(value)
		}
	}()

	select {
	case <- time.After(time.Hour):
	}
}

El stopCh aquí es el canal de señal, tiene un solo remitente, por lo que se puede cerrar directamente. Después de que los remitentes reciben la señal de apagado, se selecciona la rama de selección "case <- stopCh", sale de la función y ya no envía datos.

Cabe señalar que el código anterior no cierra explícitamente dataCh. En el lenguaje Go, para un canal, si ninguna gorutina termina refiriéndose a él, gc eventualmente lo reciclará independientemente de si el canal está cerrado o no. Por lo tanto, en este caso, el llamado cierre elegante del canal significa no cerrar el canal y dejar que gc lo haga por usted.

En el último caso, la forma de cerrar elegantemente el canal es: cualquiera de ellos dice “terminemos el juego” notificando a un moderador que cierre un canal de señal adicional.

A diferencia del tercer caso, aquí hay receptores M. Si la tercera solución se adopta directamente, si el receptor cierra directamente stopCh, un canal se cerrará repetidamente, provocando pánico. Por lo tanto, es necesario agregar un intermediario, y todos los receptores M le envían "solicitudes" para cerrar dataCh. Después de que el intermediario recibe la primera solicitud, emitirá directamente instrucciones para cerrar dataCh (al cerrar stopCh, no se producirán cierres repetidos .situación, porque el remitente de stopCh sólo tiene un intermediario). Además, los N remitentes aquí también pueden enviar solicitudes para cerrar dataCh al intermediario.

func main() {
	rand.Seed(time.Now().UnixNano())

	const Max = 100000
	const NumReceivers = 10
	const NumSenders = 1000

	dataCh := make(chan int, 100)
	stopCh := make(chan struct{})

	// It must be a buffered channel.
	toStop := make(chan string, 1)

	var stoppedBy string

	// moderator
	go func() {
		stoppedBy = <-toStop
		close(stopCh)
	}()

	// senders
	for i := 0; i < NumSenders; i++ {
		go func(id string) {
			for {
				value := rand.Intn(Max)
				if value == 0 {
					select {
					case toStop <- "sender#" + id:
					default:
					}
					return
				}

				select {
				case <- stopCh:
					return
				case dataCh <- value:
				}
			}
		}(strconv.Itoa(i))
	}

	// receivers
	for i := 0; i < NumReceivers; i++ {
		go func(id string) {
			for {
				select {
				case <- stopCh:
					return
				case value := <-dataCh:
					if value == Max-1 {
						select {
						case toStop <- "receiver#" + id:
						default:
						}
						return
					}

					fmt.Println(value)
				}
			}
		}(strconv.Itoa(i))
	}

	select {
	case <- time.After(time.Hour):
	}

}

En el código, toStop desempeña el papel de intermediario y se utiliza para recibir la solicitud de cierre de dataCh enviada por el remitente y el receptor.

Aquí toStop se declara como un canal almacenado en búfer. Suponiendo que toStop declara un canal sin búfer, es posible que se pierda la primera solicitud cerrada de dataCh enviada. Debido a que tanto el remitente como el receptor envían solicitudes a través de declaraciones de selección, si la rutina donde se encuentra el intermediario no está lista, la declaración de selección no la seleccionará y usará la opción predeterminada directamente sin hacer nada. De esta forma, se perderá la primera solicitud para cerrar dataCh.

Si declaramos la capacidad de toStop como Num(remitentes) + Num(receptores), entonces la parte que envía la solicitud dataCh se puede cambiar a una forma más concisa:

...
toStop := make(chan string, NumReceivers + NumSenders)
...
			value := rand.Intn(Max)
			if value == 0 {
				toStop <- "sender#" + id
				return
			}
...
				if value == Max-1 {
					toStop <- "receiver#" + id
					return
				}
...

Envíe una solicitud directamente a toStop. Debido a que la capacidad de toStop es lo suficientemente grande, no hay necesidad de preocuparse por el bloqueo. Naturalmente, no es necesario agregar una declaración de selección y un caso predeterminado para evitar el bloqueo.

Se puede ver que dataCh en realidad no está cerrado aquí, y es lo mismo que en el tercer caso.

Las anteriores son las situaciones más básicas, pero pueden cubrir casi todas las situaciones y sus variaciones. Solo recuerda:

no cierre un canal desde el lado del receptor y no cierre un canal si el canal tiene varios remitentes simultáneos.

Y principios más esenciales:

no cierre (ni envíe valores a) canales cerrados.

Supongo que te gusta

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