2012-12-02 11 views
30

Ich habe zwei Göroutinen, die unabhängig voneinander Daten produzieren, die sie jeweils an einen Kanal senden. In meiner Haupt-Routine möchte ich jeden dieser Ausgänge konsumieren, so wie sie kommen, aber die Reihenfolge, in der sie eingehen, interessiert mich nicht. Jeder Kanal schließt sich selbst, wenn er seine Ausgabe erschöpft hat. Während die select-Anweisung die einfachste Syntax ist, um Eingaben unabhängig voneinander zu konsumieren, habe ich keinen übersichtlichen Weg gesehen, um jeden einzelnen zu durchlaufen, bis beide Kanäle geschlossen sind.Ausbrechen einer Select-Anweisung, wenn alle Kanäle geschlossen sind

for { 
    select { 
    case p, ok := <-mins: 
     if ok { 
      fmt.Println("Min:", p) //consume output 
     } 
    case p, ok := <-maxs: 
     if ok { 
      fmt.Println("Max:", p) //consume output 
     } 
    //default: //can't guarantee this won't happen while channels are open 
    // break //ideally I would leave the infinite loop 
       //only when both channels are done 
    } 
} 

das Beste, was ich denken kann, zu tun, ist die folgende (nur skizziert, haben Fehler kompilieren können):

for { 
    minDone, maxDone := false, false 
    select { 
    case p, ok := <-mins: 
     if ok { 
      fmt.Println("Min:", p) //consume output 
     } else { 
      minDone = true 
     } 
    case p, ok := <-maxs: 
     if ok { 
      fmt.Println("Max:", p) //consume output 
     } else { 
      maxDone = true 
     } 
    } 
    if (minDone && maxDone) {break} 
} 

aber das sieht aus wie wäre es nicht haltbar, wenn man mit mehr arbeiten als zwei oder drei Kanäle. Die einzige andere Methode, die ich kenne, besteht darin, einen Timout-Fall in der switch-Anweisung zu verwenden, der entweder klein genug ist, um ein frühes Verlassen zu riskieren, oder zu viel Ausfallzeit in die letzte Schleife injiziert. Gibt es eine bessere Möglichkeit, Kanäle zu testen, die sich in einer SELECT-Anweisung befinden?

Antwort

58

Ihre Beispiellösung würde nicht gut funktionieren. Sobald einer von ihnen geschlossen wurde, war er immer sofort für die Kommunikation verfügbar. Das bedeutet, dass Ihre Goroutine niemals nachgibt und andere Kanäle möglicherweise nie bereit sind. Sie würden effektiv in eine Endlosschleife eintreten. Ich postete ein Beispiel, um den Effekt hier zu veranschaulichen: http://play.golang.org/p/rOjdvnji49

Also, wie würde ich dieses Problem lösen? Ein Nullkanal ist niemals für die Kommunikation bereit. Jedes Mal, wenn Sie auf einen geschlossenen Kanal stoßen, können Sie diesen Kanal auf Null stellen und sicherstellen, dass er nie wieder ausgewählt wird. Fahrbare Beispiel hier: http://play.golang.org/p/8lkV_Hffyj

for { 
    select { 
    case x, ok := <-ch: 
     fmt.Println("ch1", x, ok) 
     if !ok { 
      ch = nil 
     } 
    case x, ok := <-ch2: 
     fmt.Println("ch2", x, ok) 
     if !ok { 
      ch2 = nil 
     } 
    } 

    if ch == nil && ch2 == nil { 
     break 
    } 
} 

Was ist Angst davor immer unhandlich, ich glaube nicht, es wird. Es ist sehr selten, dass Kanäle zu viele Orte gleichzeitig haben. Dies würde so selten vorkommen, dass mein erster Vorschlag darin besteht, mich damit zu befassen. Eine lange if-Anweisung, die 10 Kanäle mit Null vergleicht, ist nicht der schlechteste Teil, wenn man versucht, mit 10 Kanälen in einer Auswahl umzugehen.

+1

Ich würde wahrscheinlich diese 'if' Anweisung in eine lokale Funktion umbrechen. Es wäre weniger Unordnung und der Funktionsname würde dazu beitragen, es offensichtlicher zu machen, was geschah (etwas in dieser Richtung: http://play.golang.org/p/347KZdI_Gs). – Chris

+2

+1 für die nicht-clevere Lösung. Es ist wichtig, diese Kanäle auf Null zu setzen, damit sie keine Zeit verschwenden, aber ich könnte eine separate Variable verwenden, um die Anzahl der offenen Kanäle herunterzuzählen.für n = 2; n> 0; {und dann jedes Mal, wenn Sie einen Kanal auf Null setzen, n--. – Sonia

+1

Aus der Sprachspezifikation: "Da die Kommunikation auf Null-Kanälen niemals fortgesetzt werden kann, wählen Sie mit nur Null-Kanälen und keine Standard-Case-Blöcke für immer." Warum blockiert das Obige nicht für immer? [Edit: Oh warte, yep die Pause wird immer vor der ersten Iteration der Schleife, wo beide sind Null] – voutasaurus

17

Schließen ist in einigen Situationen schön, aber nicht alle. Ich würde es hier nicht benutzen. Stattdessen würde ich nur getan Kanal verwenden:

for n := 2; n > 0; { 
    select { 
    case p := <-mins: 
     fmt.Println("Min:", p) //consume output 
    case p := <-maxs: 
     fmt.Println("Max:", p) //consume output 
    case <-done: 
     n-- 
    } 
} 

komplettes Arbeitsbeispiel auf dem Spielplatz: http://play.golang.org/p/Cqd3lg435y

+0

Schöne Lösung! Mehr idiomatische gehen als Stephen's Lösung IMHO. Es ist leicht, sich daran zu stören, die Kanäle zu schließen, so dass eine Erinnerung, dass Sie nicht müssen, sehr nützlich ist. –

+3

Danke, Nick. Ich wollte eine Alternative zeigen, aber nachdem ich darauf geschlafen habe, denke ich, dass Stephen's und jnmls Lösungen robuster sind. Wenn Sie zum Beispiel die scheinbar einfache Änderung der Pufferminuten und -maxima in meiner Lösung vornehmen, führen Sie ein Datenrennen mit dem Kanal done ein, und das Programm würde alle Ausgaben im Puffer löschen, wenn der Wert done angekommen ist. Außerdem beantworte ich nicht streng die Frage, wie sie gestellt wurde, und spekuliere, dass das OP die Kontrolle über den Erzeugercode hat. Es ist möglich, dass er es nicht tut und dass er damit umgehen muss, dass ein Kanal geschlossen wird, wie er beschrieben hat. – Sonia

+0

Ich denke, dies könnte klarer sein mit einer [WaitGroup] (https://golang.org/pkg/sync/#WaitGroup) – FrontierPsycho

6

Warum nicht goroutines verwenden? Wenn deine Kanäle geschlossen werden, verwandelt sich das Ganze in eine einfache Bereichsschleife.

func foo(c chan whatever, prefix s) { 
     for v := range c { 
       fmt.Println(prefix, v) 
     } 
} 

// ... 

go foo(mins, "Min:") 
go foo(maxs, "Max:") 
+1

Dies war eigentlich die erste Lösung, die mir in den Sinn kam. Wenn Sie die routine calling go foo wissen wollten, wenn foo abgeschlossen wurde, müssten Sie die Synchronisation dafür hinzufügen. Auch fmt.Println ist nicht Thread-sicher und Sie können gelegentlich verstümmelte Ausgabe bekommen. log.Println ist eine einfache Alternative, die Thread-sicher ist. – Sonia

+0

@Sonia, Sie können auch 'log.Println' in einer separaten Datei verwenden und Nachrichten an sie senden. [Spielplatz] (http://play.golang.org/p/rurgmxrei4). –

+0

@Sonia, habe ich das [Paket] (https://github.com/logrusorgru/lg) gemacht. Boo-ha-ha-ha-ha. –

Verwandte Themen