2012-04-10 10 views
38

Estoy tratando de comprender la concurrencia en Go. En particular, escribí este programa hilos inseguros:Comprensión de las rutinas

package main 

import "fmt" 

var x = 1 

func inc_x() { //test 
    for { 
    x += 1 
    } 
} 

func main() { 
    go inc_x() 
    for { 
    fmt.Println(x) 
    } 
} 

Reconozco que debo utilizar los canales para evitar las condiciones de carrera con x, pero ese no es el punto aquí. El programa imprime 1 y luego parece que se repite para siempre (sin imprimir nada más). Esperaría que imprima una lista infinita de números, posiblemente omitiendo algunos y repitiendo otros debido a la condición de carrera (o peor, imprimiendo el número mientras se actualiza en inc_x).

Mi pregunta es: ¿Por qué el programa solo imprime una línea?

Para que quede claro: no estoy usando canales a propósito para este ejemplo de juguete.

Respuesta

32

Hay algunas cosas a tener en cuenta acerca de goroutines Go.

  1. No son subprocesos en el sentido de subprocesos de Java o C++.
    1. Son más como verdos.
  2. la marcha en tiempo de ejecución multiplexa los goroutines a través de los hilos del sistema
    1. El número de subprocesos del sistema es controlado por un entorno GOMAXPROCS variables y por defecto a 1 actualmente creo. Esto puede cambiar en el futuro.
  3. La forma en que los goroutines vuelven a su hilo actual está controlada por varias construcciones diferentes.
    1. La instrucción select puede devolver el control al hilo.
    2. enviar en un canal puede devolver el control al hilo.
    3. Hacer operaciones IO puede devolver el control al hilo.
    4. runtime.Gosched() cede explícitamente el control al hilo.

El comportamiento que está viendo es debido a que la función principal nunca cede de nuevo a la rosca y en su lugar se involucrado en un bucle ocupado y ya que no es sólo un hilo en el bucle principal tiene ningún lugar para correr.

+0

Acabo de pensar que mencionaría que desde Go 1.2, el programador puede invocarse periódicamente en la entrada de la función. Resolvería este caso, no creo, pero ayuda cuando tienes un bucle cerrado que llama a una función no en línea. –

2

No estoy seguro, pero creo que que inc_x está acaparando la CPU. Como no hay IO, no libera el control.

Encontré dos cosas que lo resolvieron. Una era llamar al runtime.GOMAXPROCS(2) al comienzo del programa y luego funcionaría, ya que ahora hay dos hilos que sirven goroutings. El otro es insertar time.Sleep(1) después de incrementar x.

17

Según this y this, algunas llamadas no pueden invocarse durante un límite de CPU (si el Goroutine nunca cede al planificador). Esto puede causar otros Goroutines para colgar si necesitan bloquear el hilo principal (tal es el caso de la llamada al sistema write() utilizado por fmt.Println())

La solución que encontré que participan llamando runtime.Gosched() en el hilo vinculados a la CPU para producir de nuevo a la planificador, de la siguiente manera:

package main 

import (
    "fmt" 
    "runtime" 
) 

var x = 1 

func inc_x() { 
    for { 
    x += 1 
    runtime.Gosched() 
    } 
} 

func main() { 
    go inc_x() 
    for { 
    fmt.Println(x) 
    } 
} 

Debido a que sólo se está realizando una operación en el goroutine, runtime.Gosched() se está llamando muy a menudo. Llamar al runtime.GOMAXPROCS(2) en init es más rápido en un orden de magnitud, pero sería muy inseguro si estuviera haciendo algo más complicado que incrementar un número (por ejemplo, tratar con matrices, estructuras, mapas, etc.).

En ese caso, la mejor práctica sería utilizar un canal para administrar el acceso compartido a un recurso.

Actualización: A partir de Go 1.2, any non-inlined function call can invoke the scheduler.

+3

¿Es correcto decir que este problema nunca ocurre con el uso adecuado de los canales? De lo contrario, parece un gran problema con Go, si un hilo puede enganchar a la CPU y no se pueden ejecutar otros hilos. –

+0

Si tiene varias CPU también puede establecer la variable de entorno en GOMAXPROCS = 2 y luego la rutina puede ejecutarse en una secuencia separada de la función principal. La función Gosched() le dice al tiempo de ejecución que ceda en ese ciclo for. –

+0

@ user793587 Depende de lo que quiere decir con "apropiado". Sondear en un circuito cerrado puede enganchar un hilo, pero el código es malo de todos modos. En la práctica, simplemente no es un problema. En el caso raro de que * necesite * sondear en un ciclo cerrado, puede ceder explícitamente al programador, pero normalmente solo aparece en los ejemplos de juguete. He oído hablar de planes para cambiar a un programador preventivo, pero el planificador cooperativo actual funciona bien en la mayoría de los casos. – SteveMcQwark

7

Es una interacción de dos cosas. Uno, de forma predeterminada, Go solo usa un solo núcleo, y dos, Go debe programar los goroutines cooperativamente. Su función inc_x no cede, por lo que monopoliza el núcleo único que se está utilizando. Aliviar cualquiera de estas condiciones dará lugar a la producción que espera.

Decir "núcleo" es un poco brillante. En realidad, Go puede usar múltiples núcleos detrás de la escena, pero usa una variable llamada GOMAXPROCS para determinar el número de subprocesos para programar sus rutinas que realizan tareas que no son del sistema. Como se explica en FAQ y Effective Go, el valor predeterminado es 1, pero puede establecerse más alto con una variable de entorno o una función de tiempo de ejecución. Es probable que esto le proporcione el resultado esperado, pero solo si su procesador tiene múltiples núcleos.

Independientemente de los núcleos y GOMAXPROCS, puede darle la oportunidad al planificador de rutina en el tiempo de ejecución de hacer su trabajo. El planificador no puede apropiarse de una rutina de ejecución pero debe esperar a que regrese al tiempo de ejecución y solicite algún servicio, como IO, time.Sleep() o runtime.Gosched(). Agregar algo como esto en inc_x produce el resultado esperado. El administrador de goroutine main() ya está solicitando un servicio con fmt.Println, por lo que con las dos rutinas que ahora periódicamente ceden al tiempo de ejecución, puede hacer algún tipo de programación justa.

+0

Puede ejecutar múltiples hilos/procesos simultáneos en un procesador, por lo que "solo si su procesador tiene múltiples núcleos" es engañoso. – lunixbochs

Cuestiones relacionadas