come ascoltare N canali? (istruzione di selezione dinamica)


116

per avviare un ciclo infinito di esecuzione di due goroutine, posso utilizzare il codice seguente:

dopo aver ricevuto il messaggio inizierà una nuova goroutine e andrà avanti per sempre.

c1 := make(chan string)
c2 := make(chan string)

go DoStuff(c1, 5)
go DoStuff(c2, 2)

for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

Ora vorrei avere lo stesso comportamento per le N goroutine, ma come apparirà l'istruzione select in quel caso?

Questo è il bit di codice con cui ho iniziato, ma sono confuso su come codificare l'istruzione select

numChans := 2

//I keep the channels in this slice, and want to "loop" over them in the select statemnt
var chans = [] chan string{}

for i:=0;i<numChans;i++{
    tmp := make(chan string);
    chans = append(chans, tmp);
    go DoStuff(tmp, i + 1)

//How shall the select statment be coded for this case?  
for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

4
Penso che quello che vuoi sia Channel Multiplexing. golang.org/doc/effective_go.html#chan_of_chan Fondamentalmente, hai un solo canale che ascolti e poi più canali figli che si incanalano nel canale principale. Domanda SO correlata: stackoverflow.com/questions/10979608/…
Brenden

Risposte:


152

Puoi farlo usando la Selectfunzione dal pacchetto di riflettere :

func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)

Seleziona esegue un'operazione di selezione descritta dall'elenco dei casi. Come l'istruzione Go select, si blocca finché almeno uno dei casi non può procedere, effettua una scelta pseudocasuale uniforme, quindi esegue quel caso. Restituisce l'indice del caso scelto e, se quel caso era un'operazione di ricezione, il valore ricevuto e un valore booleano che indica se il valore corrisponde a un invio sul canale (al contrario di un valore zero ricevuto perché il canale è chiuso).

Si passa in una matrice di SelectCasestrutture che identificano il canale su cui selezionare, la direzione dell'operazione e un valore da inviare nel caso di un'operazione di invio.

Quindi potresti fare qualcosa del genere:

cases := make([]reflect.SelectCase, len(chans))
for i, ch := range chans {
    cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
}
chosen, value, ok := reflect.Select(cases)
// ok will be true if the channel has not been closed.
ch := chans[chosen]
msg := value.String()

Puoi sperimentare un esempio più dettagliato qui: http://play.golang.org/p/8zwvSk4kjx


4
Esiste un limite pratico al numero di casi in tale selezione? Quella che se si va oltre, le prestazioni sono fortemente influenzate?
Maxim Vladimirsky

4
Forse è la mia incompetenza, ma ho trovato molto difficile lavorare con questo schema quando invii e ricevi strutture complesse attraverso il canale. Passare un canale "aggregato" condiviso, come ha detto Tim Allclair, è stato molto più facile nel mio caso.
Bora M. Alper

90

Puoi ottenere questo risultato avvolgendo ogni canale in una goroutine che "inoltra" i messaggi a un canale "aggregato" condiviso. Per esempio:

agg := make(chan string)
for _, ch := range chans {
  go func(c chan string) {
    for msg := range c {
      agg <- msg
    }
  }(ch)
}

select {
case msg <- agg:
    fmt.Println("received ", msg)
}

Se hai bisogno di sapere da quale canale ha avuto origine il messaggio, puoi racchiuderlo in una struttura con qualsiasi informazione aggiuntiva prima di inoltrarlo al canale aggregato.

Nei miei test (limitati), questo metodo supera di gran lunga le prestazioni utilizzando il pacchetto di riflessione:

$ go test dynamic_select_test.go -test.bench=.
...
BenchmarkReflectSelect         1    5265109013 ns/op
BenchmarkGoSelect             20      81911344 ns/op
ok      command-line-arguments  9.463s

Codice di benchmark qui


2
Il codice del benchmark non è corretto, è necessario eseguire il loop sub.N un benchmark. Altrimenti i risultati (che sono divisi per b.N, 1 e 2000000000 nell'output) saranno completamente privi di significato.
Dave C

2
@ DaveC Grazie! La conclusione non cambia, ma i risultati sono molto più sani.
Tim Allclair

1
In effetti, ho fatto un rapido hack sul tuo codice di benchmark per ottenere alcuni numeri reali . Potrebbe esserci ancora qualcosa di sbagliato / mancante da questo benchmark, ma l'unica cosa che il codice di riflessione più complicato ha da fare è che l'installazione è più veloce (con GOMAXPROCS = 1) poiché non ha bisogno di un mucchio di goroutine. In ogni altro caso un semplice canale di fusione goroutine spazza via la soluzione di riflessione (di ~ 2 ordini di grandezza).
Dave C

2
Uno svantaggio importante (rispetto reflect.Selectall'approccio) è che le goroutine eseguono il buffer di fusione come minimo un singolo valore su ciascun canale che viene unito. Di solito non sarà un problema, ma in alcune applicazioni specifiche potrebbe essere un problema :(.
Dave C,

1
un canale di unione bufferizzato peggiora il problema. Il problema è che solo la soluzione di riflessione può avere una semantica completamente senza buffer. Sono andato avanti e ho pubblicato il codice di test che stavo sperimentando come risposta separata per (si spera) chiarire cosa stavo cercando di dire.
Dave C

22

Per espandere alcuni commenti sulle risposte precedenti e per fornire un confronto più chiaro, ecco un esempio di entrambi gli approcci presentati finora con lo stesso input, una fetta di canali da cui leggere e una funzione da chiamare per ogni valore che deve anche sapere quale canale da cui proviene il valore.

Esistono tre differenze principali tra gli approcci:

  • Complessità. Sebbene possa essere parzialmente una preferenza del lettore, trovo l'approccio al canale più idiomatico, diretto e leggibile.

  • Prestazione. Sul mio sistema Xeon amd64 le goroutines + channel out eseguono la soluzione di riflessione di circa due ordini di grandezza (in generale la riflessione in Go è spesso più lenta e dovrebbe essere usata solo quando assolutamente necessario). Ovviamente, se si verifica un ritardo significativo nella funzione che elabora i risultati o nella scrittura dei valori sui canali di ingresso, questa differenza di prestazioni può facilmente diventare insignificante.

  • Semantica di blocco / buffering. L'importanza di questo dipende dal caso d'uso. Molto spesso non avrà importanza o il leggero buffering aggiuntivo nella soluzione di fusione goroutine potrebbe essere utile per il throughput. Tuttavia, se è desiderabile avere la semantica che solo un singolo scrittore è sbloccato e il suo valore è completamente gestito prima che qualsiasi altro scrittore venga sbloccato, allora ciò può essere ottenuto solo con la soluzione di riflessione.

Nota, entrambi gli approcci possono essere semplificati se l '"id" del canale di invio non è richiesto o se i canali di origine non verranno mai chiusi.

Canale di fusione Goroutine:

// Process1 calls `fn` for each value received from any of the `chans`
// channels. The arguments to `fn` are the index of the channel the
// value came from and the string value. Process1 returns once all the
// channels are closed.
func Process1(chans []<-chan string, fn func(int, string)) {
    // Setup
    type item struct {
        int    // index of which channel this came from
        string // the actual string item
    }
    merged := make(chan item)
    var wg sync.WaitGroup
    wg.Add(len(chans))
    for i, c := range chans {
        go func(i int, c <-chan string) {
            // Reads and buffers a single item from `c` before
            // we even know if we can write to `merged`.
            //
            // Go doesn't provide a way to do something like:
            //     merged <- (<-c)
            // atomically, where we delay the read from `c`
            // until we can write to `merged`. The read from
            // `c` will always happen first (blocking as
            // required) and then we block on `merged` (with
            // either the above or the below syntax making
            // no difference).
            for s := range c {
                merged <- item{i, s}
            }
            // If/when this input channel is closed we just stop
            // writing to the merged channel and via the WaitGroup
            // let it be known there is one fewer channel active.
            wg.Done()
        }(i, c)
    }
    // One extra goroutine to watch for all the merging goroutines to
    // be finished and then close the merged channel.
    go func() {
        wg.Wait()
        close(merged)
    }()

    // "select-like" loop
    for i := range merged {
        // Process each value
        fn(i.int, i.string)
    }
}

Riflessione seleziona:

// Process2 is identical to Process1 except that it uses the reflect
// package to select and read from the input channels which guarantees
// there is only one value "in-flight" (i.e. when `fn` is called only
// a single send on a single channel will have succeeded, the rest will
// be blocked). It is approximately two orders of magnitude slower than
// Process1 (which is still insignificant if their is a significant
// delay between incoming values or if `fn` runs for a significant
// time).
func Process2(chans []<-chan string, fn func(int, string)) {
    // Setup
    cases := make([]reflect.SelectCase, len(chans))
    // `ids` maps the index within cases to the original `chans` index.
    ids := make([]int, len(chans))
    for i, c := range chans {
        cases[i] = reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(c),
        }
        ids[i] = i
    }

    // Select loop
    for len(cases) > 0 {
        // A difference here from the merging goroutines is
        // that `v` is the only value "in-flight" that any of
        // the workers have sent. All other workers are blocked
        // trying to send the single value they have calculated
        // where-as the goroutine version reads/buffers a single
        // extra value from each worker.
        i, v, ok := reflect.Select(cases)
        if !ok {
            // Channel cases[i] has been closed, remove it
            // from our slice of cases and update our ids
            // mapping as well.
            cases = append(cases[:i], cases[i+1:]...)
            ids = append(ids[:i], ids[i+1:]...)
            continue
        }

        // Process each value
        fn(ids[i], v.String())
    }
}

[Codice completo nel playground Go .]


1
Vale anche la pena notare che la soluzione goroutines + channels non può fare tutto selecto reflect.Selectfa. Le goroutine continueranno a girare fino a quando non consumeranno tutto dai canali, quindi non c'è un modo chiaro per Process1uscire presto. C'è anche il rischio di problemi se si hanno più lettori, poiché le goroutine memorizzano un elemento da ciascuno dei canali, cosa che non accadrà con select.
James Henstridge

@JamesHenstridge, la tua prima nota sull'arresto non è vera. Organizzeresti di arrestare Process1 esattamente nello stesso modo in cui organizzeresti per arrestare Process2; es. aggiungendo un canale "stop" che viene chiuso quando le goroutine dovrebbero fermarsi. Process1 avrebbe bisogno di due casi selectall'interno di un forciclo invece del for rangeciclo più semplice attualmente utilizzato. Process2 avrebbe bisogno di inserire un altro caso casese gestire in modo speciale quel valore di i.
Dave C

Ciò ancora non risolve il problema che stai leggendo valori dai canali che non verranno utilizzati nel caso di arresto anticipato.
James Henstridge,

0

Perché questo approccio non funzionerebbe supponendo che qualcuno stia inviando eventi?

func main() {
    numChans := 2
    var chans = []chan string{}

    for i := 0; i < numChans; i++ {
        tmp := make(chan string)
        chans = append(chans, tmp)
    }

    for true {
        for i, c := range chans {
            select {
            case x = <-c:
                fmt.Printf("received %d \n", i)
                go DoShit(x, i)
            default: continue
            }
        }
    }
}

8
Questo è uno spin-loop. In attesa che un canale di ingresso abbia un valore, questo consuma tutta la CPU disponibile. Il punto centrale di selectsu più canali (senza una defaultclausola) è che attende in modo efficiente finché almeno uno è pronto senza girare.
Dave C

0

Opzione forse più semplice:

Invece di avere una serie di canali, perché non passare un solo canale come parametro alle funzioni eseguite su goroutine separate e quindi ascoltare il canale in una goroutine consumer?

Ciò ti consente di selezionare un solo canale nel tuo ascoltatore, effettuando una semplice selezione ed evitando la creazione di nuove goroutine per aggregare messaggi da più canali?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.