2012-04-10 16 views
38

Ich versuche, Nebenläufigkeit in Go zu verstehen. Insbesondere schrieb ich dieses Thread-unsicheres Programm:Understanding goroutines

package main 

import "fmt" 

var x = 1 

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

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

ich erkennen, dass ich Kanäle verwenden sollte Rennbedingungen mit x zu verhindern, aber das ist nicht der Punkt hier. Das Programm druckt 1 und scheint dann für immer zu loopen (ohne mehr zu drucken). Ich würde erwarten, dass es eine unendliche Liste von Zahlen druckt, möglicherweise einige überspringt und andere aufgrund der Wettlaufsituation wiederholt (oder schlimmer - die Nummer wird gedruckt, während sie in inc_x aktualisiert wird).

Meine Frage ist: Warum druckt das Programm nur eine Zeile?

Nur um klar zu sein: Ich benutze keine Kanäle für dieses Spielzeug Beispiel.

Antwort

32

Es gibt ein paar Dinge zu beachten, über Gos goroutines zu halten.

  1. Sie sind keine Threads im Sinne von Java oder C++ - Threads.
    1. Sie sind mehr wie Greenlets.
  2. Die Go-Laufzeit multiplext die goroutines über die System-Threads
    1. Die Anzahl der System-Threads wird durch eine Umgebungsvariable GOMAXPROCS und Vorgaben gesteuert 1 zur Zeit denke ich. Dies kann sich in Zukunft ändern.
  3. Die Art und Weise, wie Gatorutine zu ihrem aktuellen Thread zurückkehren, wird von mehreren verschiedenen Konstrukten gesteuert.
    1. Die SELECT-Anweisung kann die Steuerung zurück an den Thread übergeben.
    2. Senden auf einem Kanal kann die Steuerung zurück zum Thread führen.
    3. Das Ausführen von IO-Operationen kann die Steuerung zurück zum Thread steuern.
    4. runtime.Gosched() gibt die Steuerung explizit an den Thread zurück.

Das Verhalten, das Sie sehen, ist, weil die Hauptfunktion nie wieder auf das Gewinde ergibt und stattdessen in einem langen Schleife beteiligt ist und da nur ein Thread hat die Hauptschleife ist kein Platz zu laufen.

+0

Nur gedacht, ich würde erwähnen, dass seit Go 1.2 der Scheduler bei der Funktionseingabe in regelmäßigen Abständen aufgerufen werden kann. Es würde diesen Fall lösen, denke ich nicht, aber es hilft, wenn Sie eine enge Schleife haben, die eine nicht-inlined Funktion aufruft. –

2

Kein sicher, aber ich denke, dass inc_x ist die CPU hogging. Da es kein IO gibt, gibt es keine Kontrolle frei.

Ich habe zwei Dinge gefunden, die das gelöst haben. Man sollte runtime.GOMAXPROCS(2) am Anfang des Programms anrufen und dann wird es funktionieren, da es jetzt zwei Threads gibt, die Göroutings bedienen. Der andere ist time.Sleep(1) nach dem Inkrementieren x einfügen.

17

Gemäß this und this können einige Aufrufe während einer CPU-gebundenen Goroutine nicht aufgerufen werden (wenn die Goroutine dem Scheduler niemals nachgibt). Dies kann andere Goroutines abstürzen, wenn sie den Haupt-Thread blockieren müssen (wie der Fall mit dem write() syscall von fmt.Println() verwendet wird)

Die Lösung, die ich runtime.Gosched() in Ihrer CPU-bound Thread Aufruf beteiligten GEFUNDEN die ergeben zurück Scheduler wie folgt:

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) 
    } 
} 

da Sie nur eine Operation in der Goroutine Durchführung wird runtime.Gosched()sehr oft genannt wird. Das Aufrufen von runtime.GOMAXPROCS(2) auf init ist um eine Größenordnung schneller, wäre aber sehr thread-unsicher, wenn Sie etwas komplizierteres tun würden, als eine Zahl zu erhöhen (z. B. mit Arrays, Structs, Maps usw. zu arbeiten).

In diesem Fall würden Best Practices möglicherweise einen Kanal verwenden, um den gemeinsamen Zugriff auf eine Ressource zu verwalten.

Update: Ab Go 1.2, any non-inlined function call can invoke the scheduler.

+3

Ist es richtig zu sagen, dass dieses Problem bei richtiger Verwendung der Kanäle nie auftritt? Ansonsten scheint es ein großes Problem mit Go zu sein - wenn ein Thread die CPU hacken kann und keine anderen Threads ausgeführt werden. –

+0

Wenn Sie mehrere CPUs haben, können Sie die Umgebungsvariable auch auf GOMAXPROCS = 2 setzen, und dann kann die Goroutine in einem separaten Thread als die Hauptfunktion ausgeführt werden. Die Funktion Gosched() teilt der Laufzeit mit, dass sie in dieser For-Schleife ausgeben soll. –

+0

@ user793587 Hängt davon ab, was Sie mit "richtig" meinen. Polling in einer engen Schleife kann einen Thread hoggen, aber der Code ist sowieso schlecht. In der Praxis ist das kein Problem. In dem seltenen Fall, dass * Sie * in einer engen Schleife abfragen müssen, können Sie dem Scheduler explizit nachgeben, aber er kommt normalerweise nur in Spielzeugbeispielen vor. Ich habe von Plänen gehört, zu einem preemptive Scheduler zu wechseln, aber der aktuelle kooperative Scheduler funktioniert in den meisten Fällen gut. – SteveMcQwark

7

Es ist eine Interaktion von zwei Dingen. Einerseits verwendet Go nur einen einzelnen Kern und zweitens muss Go Gououtines kooperativ planen. Ihre Funktion inc_x gibt nicht nach und so monopolisiert sie den verwendeten Single-Core. Die Entlastung einer dieser Bedingungen führt zu der erwarteten Ausgabe.

Sagen "Kern" ist ein bisschen ein Glanz. Go verwendet möglicherweise mehrere Kerne hinter den Kulissen, aber es verwendet eine Variable namens GOMAXPROCS, um die Anzahl der Threads zu bestimmen, die Ihre Gououtines planen, die nicht-systemische Aufgaben ausführen. Wie in den Artikeln FAQ und Effective Go erläutert, ist der Standardwert 1, aber er kann mit einer Umgebungsvariablen oder einer Laufzeitfunktion höher gesetzt werden. Dies wird wahrscheinlich die erwartete Ausgabe liefern, aber nur wenn Ihr Prozessor mehrere Kerne hat.

Unabhängig von Kernen und GOMAXPROCS können Sie dem Goroutine-Scheduler in der Laufzeit eine Chance geben, seine Aufgabe zu erledigen. Der Scheduler kann eine laufende Goroutine nicht vormerken, muss jedoch darauf warten, dass er zur Laufzeit zurückkehrt und einen Dienst wie IO, time.Sleep() oder runtime.Gosched() anfordert. Wenn Sie etwas in inc_x hinzufügen, wird die erwartete Ausgabe erzeugt. Die goroutine, die main() ausführt, fordert bereits einen Dienst mit fmt.Println an, so dass die zwei Grouutinen, die nun periodisch der Laufzeit nachgeben, eine faire Planung durchführen kann.

+0

Sie können mehrere gleichzeitige Threads/Prozesse auf einem Prozessor ausführen, also "nur wenn Ihr Prozessor mehrere Kerne hat" ist irreführend. – lunixbochs