multithreading - goroutines - Bloqueo de golang y no bloqueo.
golang multicore (3)
Debe leer primero la respuesta de @Not_a_Golfer y el enlace que proporcionó para comprender cómo se programan los goroutines. Mi respuesta es más parecida a una inmersión por inmersión en la red IO específicamente. Supongo que entiendes cómo Go logra la multitarea cooperativa.
Go puede y solo usa llamadas de bloqueo porque todo funciona en goroutines y no son hilos reales del sistema operativo. Son hilos verdes. Por lo tanto, puede tener muchos de ellos bloqueando todas las llamadas de IO y no se comen toda su memoria y CPU como lo harían los hilos del SO.
El archivo IO es solo syscalls. Not_a_Golfer ya cubrió eso. Go utilizará el hilo del sistema operativo real para esperar en un syscall y desbloqueará el goroutine cuando regrese. Here puedes ver la implementación de read
archivos para Unix.
La red IO es diferente. El tiempo de ejecución utiliza el "sondeador de red" para determinar qué goroutine debería desbloquearse de la llamada IO. Dependiendo del sistema operativo de destino, utilizará las API asíncronas disponibles para esperar los eventos de IO de la red. Las llamadas parecen bloquear pero dentro de todo se hace de forma asíncrona.
Por ejemplo, cuando se llama a la read
en el socket TCP, la goroutine intentará leer usando syscall. Si no llega nada, se bloqueará y esperará a que se reanude. Al bloquear aquí me refiero al estacionamiento que pone a la goroutina en una cola donde espera reanudarse. Así es como el goroutine "bloqueado" rinde ejecución a otros goroutines cuando usa la red IO.
func (fd *netFD) Read(p []byte) (n int, err error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if err := fd.pd.PrepareRead(); err != nil {
return 0, err
}
for {
n, err = syscall.Read(fd.sysfd, p)
if err != nil {
n = 0
if err == syscall.EAGAIN {
if err = fd.pd.WaitRead(); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
break
}
if _, ok := err.(syscall.Errno); ok {
err = os.NewSyscallError("read", err)
}
return
}
https://golang.org/src/net/fd_unix.go?s=#L237
Cuando lleguen los datos, el encuestador de la red devolverá goroutines que deberían reanudarse. Puede ver here función de búsqueda que se puede ejecutar que busca goroutines que se pueden ejecutar. Llama a la función netpoll
que devolverá goroutines que se pueden reanudar. Puedes encontrar la implementación de netpoll
de netpoll
here .
En cuanto a async / espera en c #. La IO de red asíncrona también utilizará API asíncronas (puertos de finalización de IO en Windows). Cuando llega algo, el sistema operativo ejecutará la devolución de llamada en uno de los subprocesos del puerto de finalización del conjunto de subprocesos, lo que pondrá continuidad en el actual SynchronizationContext
. En cierto sentido, hay algunas similitudes (aparcar / desempaquetar parece ser una continuación de llamadas pero en un nivel mucho más bajo) pero estos modelos son muy diferentes, por no mencionar las implementaciones. Los Goroutines por defecto no están vinculados a un subproceso del sistema operativo específico, se pueden reanudar en cualquiera de ellos, no importa. No hay hilos de interfaz de usuario para tratar. Async / await se realizan específicamente con el propósito de reanudar el trabajo en el mismo subproceso del sistema operativo utilizando SynchronizationContext
. Y debido a que no hay subprocesos verdes o un programador separado, async / await tiene que dividir su función en múltiples devoluciones de llamada que se ejecutan en SynchronizationContext
que es básicamente un bucle infinito que verifica una cola de devoluciones de llamada que deben ejecutarse. Incluso puedes implementarlo tú mismo, es muy fácil.
Estoy un poco confundido sobre cómo Go maneja la IO no bloqueante. La mayoría de las API parecen sincrónicas para mí, y cuando veo presentaciones en Go, no es raro escuchar comentarios como "y los bloques de llamadas"
¿Go utiliza el bloqueo de E / S cuando lee archivos o redes? ¿O hay algún tipo de magia que reescribe el código cuando se usa desde una rutina Go?
Al provenir de un fondo de C #, esto se siente muy no intuitivo, en C # tenemos la palabra clave de await
cuando consumimos API asíncronas. Lo que comunica claramente que la API puede generar el hilo actual y continuar más tarde dentro de una continuación.
Entonces TLDR; ¿Bloqueará Go el subproceso actual cuando realice IO dentro de una rutina Go, o se transformará en un C # como async a la espera de la máquina de estado usando continuaciones?
Go bloqueará la goroutine actual cuando realice IO o syscalls, pero cuando esto sucede, se permitirá que otra goroutine se ejecute en lugar de la goroutine bloqueada. Las versiones anteriores de Go solo permitían una ejecución de goroutine a la vez, pero desde 1.5, ese número ha cambiado a la cantidad de cpu cores disponibles. ( runtime.GOMAXPROCS )
No tienes que preocuparte por bloquear en Go. Por ejemplo, el servidor http de la biblioteca estándar ejecuta sus funciones de controlador en una goroutine. Si intenta leer un archivo mientras se entrega una solicitud http, se bloqueará, pero si aparece otra solicitud mientras la primera está bloqueada, se permitirá que otra goroutine ejecute y atienda esa solicitud. Luego, cuando se hace la segunda goroutina, y la primera ya no está bloqueada, se reanudará (si GOMAXPROCS> 1, la goroutina bloqueada podría reanudarse incluso antes si hay un hilo libre).
Para más información, echa un vistazo a estos:
Go tiene un programador que le permite escribir código síncrono, realiza el cambio de contexto por sí mismo y utiliza IO asíncrono bajo el capó. Entonces, si está ejecutando varios goroutines, pueden ejecutarse en un solo hilo del sistema, y cuando su código se está bloqueando desde la vista del goroutine, no está realmente bloqueando. No es magia, pero sí, te enmascara todo esto.
El programador asignará subprocesos del sistema cuando sean necesarios y durante las operaciones que realmente están bloqueando (creo que el archivo IO está bloqueando, por ejemplo, o llamando al código C). Pero si estás haciendo un simple servidor http, puedes tener miles y miles de goroutine usando en realidad un puñado de "hilos reales".
Puedes leer más sobre el funcionamiento interno de Go aquí: