2012-04-16 21 views
5

Estoy trabajando en un ejercicio de uso de canales para implementar una cola. Específicamente, estoy tratando de usar el tamaño de un canal para limitar el número de goroutines simultáneos. A saber, he escrito el siguiente código:Necesito ayuda para entender por qué select {} no está bloqueando para siempre

package main 

import "fmt" 
import "time" 
import "math/rand" 

func runTask (t string, ch *chan bool) { 
     start := time.Now() 
     fmt.Println("starting task", t) 
     time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time 
     fmt.Println("done running task", t, "in", time.Since(start)) 
     <- *ch 
} 

func main() { 
     numWorkers := 3 
     files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} 

     activeWorkers := make(chan bool, numWorkers) 

     for _, f := range files { 
       activeWorkers <- true 
       fmt.Printf("activeWorkers is %d long.\n", len(activeWorkers)) 
       go runTask(f, &activeWorkers) 
     } 
     select{} 
} 

En este momento, el código se bloquea con:

throw: all goroutines are asleep - deadlock! 

Mi expectativa era que la llamada para seleccionar bloquearía para siempre y dejar que los goroutines terminan sin punto muerto.

Así que tengo una pregunta doble: ¿por qué no seleccionar el bloqueo para siempre y, a menos de lanzar en un tiempo.Duerma() llamada después del ciclo for, ¿cómo puedo evitar interbloqueos?

Saludos,

-mtw

Respuesta

6

Arlen Cuss ya escribió una buena respuesta. Solo quiero sugerir otro diseño para tu cola de trabajo. En lugar de limitar el número de entradas que su canal puede almacenar en búfer, también puede generar un número limitado de goroutines de trabajadores que se sienten más naturales. Algo así:

package main 

import "fmt" 
import "time" 
import "math/rand" 

func runTask(t string) string { 
    start := time.Now() 
    fmt.Println("starting task", t) 
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time 
    fmt.Println("done running task", t, "in", time.Since(start)) 
    return t 
} 

func worker(in chan string, out chan string) { 
    for t := range in { 
     out <- runTask(t) 
    } 
} 

func main() { 
    numWorkers := 3 

    // spawn workers 
    in, out := make(chan string), make(chan string) 
    for i := 0; i < numWorkers; i++ { 
     go worker(in, out) 
    } 

    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} 

    // schedule tasks 
    go func() { 
     for _, f := range files { 
      in <- f 
     } 
    }() 

    // get results 
    for _ = range files { 
     <-out 
    } 
} 

También puede utilizar un sync.WaitGroup si sólo quiere esperar hasta que todas las tareas se han ejecutado, pero el uso de un canal de out tiene la ventaja de que se puede agregar los resultados más tarde. Por ejemplo, si cada tarea devuelve el número de palabras en ese archivo, el ciclo final podría usarse para resumir todos los recuentos de palabras individuales.

4

En primer lugar, no es necesario pasar un puntero al canal; canales, como mapas y otros, are references, lo que significa que los datos subyacentes no se copian, solo un puntero a los datos reales. Si necesita un puntero a un chan, sabrá cuando llegue ese momento.

El bloqueo se produce porque el programa entra en un estado en el que cada goroutine está bloqueado. Este debería ser imposible; si cada goroutine está bloqueado, entonces ningún proceso posible podría venir y despertar a otro goroutine (y su programa sería colgado en consecuencia).

El goroutine primario termina en un select {} -no está esperando a nadie, simplemente colgando. Una vez que termina la última goroutine runTask, solo queda la rutina principal, y no está esperando a nadie.

Tendrá que agregar alguna forma de saber cuándo ha terminado cada goroutine; quizás otro canal puede recibir eventos de finalización.

Esto es un poco feo, pero podría ser algo de inspiración.

package main 

import "fmt" 
import "time" 
import "math/rand" 

func runTask(t string, ch chan bool, finishedCh chan bool) { 
    start := time.Now() 
    fmt.Println("starting task", t) 
    time.Sleep(time.Millisecond * time.Duration(rand.Int31n(1500))) // fake processing time 
    fmt.Println("done running task", t, "in", time.Since(start)) 
    <-ch 
    finishedCh <- true 
} 

func main() { 
    numWorkers := 3 
    files := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"} 

    activeWorkers := make(chan bool, numWorkers) 
    finishedWorkers := make(chan bool) 
    done := make(chan bool) 

    go func() { 
     remaining := len(files) 
     for remaining > 0 { 
      <-finishedWorkers 
      remaining -= 1 
     } 

     done <- true 
    }() 

    for _, f := range files { 
     activeWorkers <- true 
     fmt.Printf("activeWorkers is %d long.\n", len(activeWorkers)) 
     go runTask(f, activeWorkers, finishedWorkers) 
    } 

    <-done 
} 
+0

Tendría cuidado con eso; todo lo que pasa pasa por valor. De hecho, los mapas y las divisiones se copian, sin embargo, parte de los datos copiados incluye punteros a los datos subyacentes. http://golang.org/doc/faq#pass_by_value. Es una distinción sutil pero importante; Los sectores tienen otros datos que no son punteros y que no se comparten. – Greg

+0

@Greg: "Los mapas y las divisiones se han copiado". Dividir pelos, creo. Según la documentación que vinculó: "Los valores de mapa y segmentación se comportan como indicadores ... Copiar un mapa o un valor de corte no copia los datos a los que apunta". Mi terminología no está en consonancia con lo que usan los documentos Go ahora, pero la semántica es en gran medida equivalente. – Ashe

+0

@Greg: he limpiado la prosa allí un poco. – Ashe

1

tux21b ya ha publicado una solución más idiomática, pero me gustaría responder a su pregunta de otra manera. select {} bloquea para siempre, sí. Un punto muerto ocurre cuando todos los goroutines están bloqueados. Si todos tus otros goroutines terminan, entonces solo tienes el goroutine principal bloqueado a la izquierda, que es un punto muerto.

Normalmente, desea hacer algo en su goroutine principal después de que todos los demás hayan terminado, ya sea mediante el uso de sus resultados, o simplemente limpiando, y para eso haría lo que sux21b sugirió.Si realmente quiere que main termine y deje el resto de los goroutines para hacer su trabajo, ponga defer runtime.Goexit() en la parte superior de su función principal. Esto hará que salga sin salir del programa.

Cuestiones relacionadas