¿Qué hace exactamente Runtime.Gosched?

86

En una versión anterior al lanzamiento de go 1.5 del sitio web Tour of Go , hay un fragmento de código que se ve así.

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

La salida se ve así:

hello
world
hello
world
hello
world
hello
world
hello

Lo que me molesta es que cuando runtime.Gosched()se quita, el programa ya no imprime "mundo".

hello
hello
hello
hello
hello

¿Por qué es así? ¿Cómo runtime.Gosched()afecta la ejecución?

Jason Yeo
fuente

Respuestas:

143

Nota:

A partir de Go 1.5, GOMAXPROCS se establece en el número de núcleos del hardware: golang.org/doc/go1.5#runtime , debajo de la respuesta original antes de 1.5.


Cuando ejecuta el programa Go sin especificar la variable de entorno GOMAXPROCS, las rutinas Go gor están programadas para su ejecución en un solo hilo del sistema operativo. Sin embargo, para hacer que el programa parezca ser multiproceso (para eso son las goroutines, ¿no?), El planificador de Go a veces debe cambiar el contexto de ejecución, por lo que cada goroutine podría hacer su trabajo.

Como dije, cuando no se especifica la variable GOMAXPROCS, el tiempo de ejecución de Go solo puede usar un hilo, por lo que es imposible cambiar los contextos de ejecución mientras goroutine está realizando algún trabajo convencional, como cálculos o incluso IO (que se asigna a funciones C simples ). El contexto se puede cambiar solo cuando se utilizan primitivas de concurrencia de Go, por ejemplo, cuando enciende varios canales, o (este es su caso) cuando le dice explícitamente al programador que cambie los contextos; para esto es runtime.Gosched.

Entonces, en resumen, cuando el contexto de ejecución en una goroutine llega a la Goschedllamada, el planificador recibe instrucciones para cambiar la ejecución a otra goroutine. En su caso, hay dos goroutines, main (que representa el hilo 'principal' del programa) y adicional, con la que ha creado go say. Si elimina la Goschedllamada, el contexto de ejecución nunca se transferirá de la primera goroutine a la segunda, por lo tanto, no habrá 'mundo' para usted. Cuando Goschedestá presente, el planificador transfiere la ejecución en cada iteración de bucle de la primera goroutine a la segunda y viceversa, por lo que tiene 'hola' y 'mundo' intercalados.

Para su información, esto se llama 'multitarea cooperativa': las gorutinas deben ceder explícitamente el control a otras gorutinas. El enfoque utilizado en la mayoría de los sistemas operativos contemporáneos se denomina 'multitarea preventiva': los hilos de ejecución no se preocupan por la transferencia de control; en su lugar, el planificador cambia los contextos de ejecución de forma transparente. El enfoque cooperativo se usa con frecuencia para implementar 'subprocesos verdes', es decir, corrutinas lógicas simultáneas que no se asignan 1: 1 a los subprocesos del sistema operativo; así es como se implementan el tiempo de ejecución de Go y sus goroutines.

Actualizar

Mencioné la variable de entorno GOMAXPROCS pero no expliqué qué es. Es hora de arreglar esto.

Cuando esta variable se establece en un número positivo N, el tiempo de ejecución de Go podrá crear hasta Nsubprocesos nativos, en los que se programarán todos los subprocesos verdes. Hilo nativo un tipo de hilo que es creado por el sistema operativo (hilos de Windows, pthreads, etc.). Esto significa que si Nes mayor que 1, es posible que goroutines se programen para ejecutarse en diferentes subprocesos nativos y, en consecuencia, se ejecuten en paralelo (al menos, hasta las capacidades de su computadora: si su sistema está basado en un procesador multinúcleo, Es probable que estos subprocesos sean verdaderamente paralelos; si su procesador tiene un solo núcleo, entonces la multitarea preventiva implementada en subprocesos del sistema operativo creará una visibilidad de ejecución paralela).

Es posible configurar la variable GOMAXPROCS usando la runtime.GOMAXPROCS()función en lugar de preestablecer la variable de entorno. Use algo como esto en su programa en lugar del actual main:

func main() {
    runtime.GOMAXPROCS(2)
    go say("world")
    say("hello")
}

En este caso puedes observar resultados interesantes. Es posible que obtenga las líneas 'hola' y 'mundo' impresas intercaladas de manera desigual, p. Ej.

hello
hello
world
hello
world
world
...

Esto puede suceder si las gorutinas están programadas para separar los subprocesos del sistema operativo. De hecho, así es como funciona la multitarea preventiva (o el procesamiento paralelo en el caso de sistemas multinúcleo): los hilos son paralelos y su salida combinada es indeterminista. Por cierto, puede dejar o eliminar la Goschedllamada, parece no tener ningún efecto cuando GOMAXPROCS es mayor que 1.

Lo siguiente es lo que obtuve en varias ejecuciones del programa con runtime.GOMAXPROCScall.

hyperplex /tmp % go run test.go
hello
hello
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
hello
hello
hello
hello
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world

Mira, a veces la salida es bonita, a veces no. Indeterminismo en acción :)

Otra actualización

Parece que en las versiones más recientes del compilador Go, el tiempo de ejecución de Go obliga a las goroutines a ceder no solo en el uso de primitivas de concurrencia, sino también en las llamadas al sistema operativo. Esto significa que el contexto de ejecución se puede cambiar entre goroutines también en llamadas a funciones IO. En consecuencia, en los compiladores de Go recientes es posible observar un comportamiento indeterminista incluso cuando GOMAXPROCS no está configurado o configurado en 1.

Vladimir Matveev
fuente
Gran trabajo ! Pero no encontré este problema en la versión 1.0.3, extraño.
WoooHaaaa
1
Esto es verdad. Acabo de verificar esto con go 1.0.3, y sí, este comportamiento no apareció: incluso con GOMAXPROCS == 1 el programa funcionó como si GOMAXPROCS> = 2. Parece que en 1.0.3 el programador ha sido modificado.
Vladimir Matveev
Creo que las cosas han cambiado con el compilador Go 1.4. El ejemplo en la pregunta de OP parece ser la creación de subprocesos del sistema operativo, mientras que esto (-> gobyexample.com/atomic-counters ) parece crear una programación cooperativa. Actualice la respuesta si esto es cierto
tez
8
A partir de Go 1.5, GOMAXPROCS se establece en la cantidad de núcleos del hardware: golang.org/doc/go1.5#runtime
thepanuto
1
@paulkon, si Gosched()es necesario o no depende de su programa, no depende del GOMAXPROCSvalor. La eficiencia de la multitarea preventiva sobre la cooperativa también depende de su programa. Si su programa está vinculado a E / S, entonces la multitarea cooperativa con E / S asíncrona probablemente será más eficiente (es decir, tendrá más rendimiento) que la E / S síncrona basada en subprocesos; si su programa está vinculado a la CPU (por ejemplo, cálculos largos), la multitarea cooperativa será mucho menos útil.
Vladimir Matveev
8

La programación cooperativa es la culpable. Sin ceder, la otra goroutine (digamos "mundo") puede tener legalmente cero oportunidades de ejecutarse antes / cuando finalice main, lo que por especificaciones termina todas las gorutines, es decir. Todo el proceso.

zzzz
fuente
1
bien, runtime.Gosched()cede. Qué significa eso? ¿Devuelve el control a la función principal?
Jason Yeo
5
En este caso específico sí. Generalmente le pide al programador que inicie y ejecute cualquiera de las rutinas "listas" en un orden de selección intencionalmente no especificado.
zzzz