Goroutinen

Goroutinen sind Funktionen, die ausgeführt werden, ohne dass das Hauptprogramm auf das Ende der Funktion wartet. Normalerweise ist ein Programmablauf wie folgt:

func main() {
    wert := macheDies()
    neuerWert := macheDas(wert)
    macheJenes(neuerWert)
}

Bevor also die zweite Funktion aufgerufen wird, beendet sich die Funktion macheDies() und die Variable wert beinhaltet den Rückgabewert der Funktion. Manchmal ist es aber nicht erwünscht, dass Funktionen nacheinander abgearbeitet werden. In einem Webserver zum Beispiel, will man möglichst viele Verbindungen »gleichzeitig« bedienen​[1]. Oder, wenn man mehrere Dateien lädt und erst nach dem Einlesen aller Dateien weiter arbeiten will, ist folgende Lösung nicht optimal:

func main() {
    datei1 := leseDatei(...)
    datei2 := leseDatei(...)
    datei3 := leseDatei(...)
    datei4 := leseDatei(...)
    datei5 := leseDatei(...)
    verarbeiteInhalte(datei1,datei2,datei3,datei4,datei5)
}

So beträgt die ausführende Zeit die Summe der einzelnen Funktionen, da sie nacheinander ausgeführt werden. Ist die erste Datei auf einem »langsamen« Laufwerk gespeichert (z.B. über das Netzwerk angebunden), dann könnte in der Zwischenzeit das System die anderen Dateien lesen und muss nicht auf die erste warten.

Goroutinen schaffen in diesen Fällen Abhilfe. Hierbei gibt jedoch einige Details, die man beachten muss. Während im ersten Beispiel mit dem Webserver die Funktionen unabhängig voneinander sind, ist das im letzten Beispiel nicht der Fall. Hier darf die Funktion verarbeiteInhalte() erst ausgeführt werden, wenn alle Dateien eingelesen werden. Somit sollen die Dateien »im Hintergrund« eingelesen werden, der Programmablauf darf aber erst fortgeführt werden, wenn die Funktionen sich beendet haben.

Eine Funktion kann in eine Goroutine umgewandelt werden, in dem man vor dem Aufruf das Schlüsselwort go schreibt.

package main

import "fmt"

func macheEtwas() {
    fmt.Println("Funktion wird im Hintergrund ausgeführt")
}

func main() {
    go macheEtwas()
    fmt.Println("Warten lohnt nicht...")
    // ...
}

Hier wird die Funktion macheEtwas() »im Hintergrund« aufgerufen und das Programm sofort fortgesetzt. Wenn das Programm gestartet wird, passiert aber etwas Merkwürdiges. Das Programm hat folgende Ausgabe:

$ go run main.go
Warten lohnt nicht...

Anscheinend wird die Funktion macheEtwas() nicht aufgerufen. Aber warum? In dem einfachen Beispiel ist das Programm nach der Ausgabe in der Funktion main() beendet. Wenn die Funktion main() beendet wird, werden automatisch auch alle Goroutinen beendet. Der interne Ressourcenverteiler (engl. Scheduler)[2] hat womöglich der Funktion noch keine Rechenkapazität zur Verfügung gestellt. Wenn das Beispiel geändert wird und wir einen länger dauernden Funktionsaufruf simulieren, erscheint die Ausgabe:

package main

import (
    "fmt"
    "time"
)

func macheEtwas() {
    fmt.Println("Funktion wird im Hintergrund ausgeführt")
}

func main() {
    go macheEtwas()
    fmt.Println("Warten lohnt nicht...")
    time.Sleep(1 * time.Second)
}

Hier wird mit time.Sleep() eine Sekunde gewartet (was für einen heutigen Rechner eine halbe Ewigkeit ist) und geben der Goroutine macheEtwas() die Chance aktiv zu werden. Damit ist die Ausgabe wie erwartet:

Warten lohnt nicht...
Funktion wird im Hintergrund ausgeführt

An dieser Stelle noch ein Hinweis: das Scheduling (also das Planen und Verteilen der Rechenzeit der einzelnen Goroutinen auf den Prozessor) ist abhängig von der konkreten Implementierung der Go-Laufzeitumgebung. Man sollte also bei Nebenläufigkeit tunlichst vermeiden sich auf Annahmen zu verlassen.

Und noch ein zweiter Hinweis, der sofort einleuchtend sein sollte: Rückgabewerte von Goroutinen werden vom System ignoriert. Es gibt ja keinen »Ort« im Programm, der das Ende einer Goroutine markiert und der die Rückgabewerte in Variablen übernehmen kann.

Synchronisieren von Goroutinen

Wenn wir Goroutinen aufrufen muss man manchmal warten, bis sich alle Goroutinen beendet haben. Vom Prinzip her also folgendes:

package main

import (
    "fmt"
    "time"
)

func macheEtwas(millisekunden time.Duration) {
    time.Sleep(millisekunden * time.Millisecond)
    fmt.Println("Funktion wird im Hintergrund ausgeführt. Dauer:", millisekunden)
}

func main() {
    go macheEtwas(200)
    go macheEtwas(400)
    go macheEtwas(150)
    go macheEtwas(600)
    // Nicht nachmachen, keine gute Vorgehensweise!
    time.Sleep(1 * time.Second)
}

Auf den ersten Blick sollte klar werden, dass ein einfaches Warten keine gute Lösung ist. Go stellt für die Synchronisation von Goroutinen spezielle Hilfsmittel bereit, eine Wartegruppe (wait group). In dieser Wartegruppe kann man Goroutinen registrieren und darauf warten, dass sich alle Goroutinen nach Beendigung wieder abgemeldet haben.

package main

import (
    "fmt"
    "sync"
    "time"
)

func macheEtwas(millisekunden time.Duration, wg *sync.WaitGroup) {
    dauer := millisekunden * time.Millisecond
    time.Sleep(dauer)
    fmt.Println("Funktion wird im Hintergrund ausgeführt. Dauer:", dauer)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(4)
    go macheEtwas(200, &wg)
    go macheEtwas(400, &wg)
    go macheEtwas(150, &wg)
    go macheEtwas(600, &wg)

    wg.Wait()
    fmt.Println("Fertig")
}

Hinweis: würde wg im Funktionsaufruf nicht als Zeiger übergeben, funktioniert das Beispiel nicht (bitte ausprobieren!). Das liegt daran, dass beim Funktionsaufruf eine Kopie von wg erstellt wird und der Aufruf von wg.Done() auf der Kopie arbeitet und somit nicht die ursprüngliche Deklaration(?) verändert.

Eine andere Möglichkeit zur Synchronisation von Goroutinen wird im nächsten Abschnitt über Kanäle (engl. channels) aufgezeigt.

Race Conditions (Wettlaufsituationen)

Goroutinen können gleichzeitig (parallel) abgearbeitet werden, wenn man mehrere Prozessorkerne hat. Selbst wenn sie nicht auf unterschiedlichen Kernen laufen, ist die Reihenfolge der Befehle nicht vorherzusehen. Damit kann es passieren, dass sich zwei Goroutinen ins Gehege kommen, wenn sie die auf denselben Variablen arbeiten. Das ist eine sogenannte Wettlaufsituation, engl. »race condition«:

package main

import (
    "fmt"
    "sync"
)

var a int

func macheEtwas(wg *sync.WaitGroup) {
    // Vorsicht! race-condition
    a = a + 1
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(4)
    go macheEtwas(&wg)
    go macheEtwas(&wg)
    go macheEtwas(&wg)
    go macheEtwas(&wg)

    wg.Wait()
    fmt.Println("Fertig")
    fmt.Println("a =", a)
}

Wenn mehrere Goroutinen gleichzeitig auf die Variable a zugreifen, kann es sein, dass sie sich gegenseitig beeinflussen. Folgender Ablauf könnte passieren:

  1. Goroutine 1 liest den Inhalt der globalen Variable a (0)
  2. Goroutine 2 liest den Inhalt der globalen Variable a (0)
  3. Goroutine 1 erhöht den gelesenen Wert um eins (1). Damit ist der gelesene Wert von Goroutine 2 nicht mehr sinnvoll.
  4. Goroutine 2 erhöht den gelesenen Wert um eins (1). Eigentlich sollte der Wert jetzt 2 sein, da Punkt 3 ausgeführt wurde.
  5. Goroutine 1 speichert den gelesenen Wert in der globalen Variablen a (1)
  6. Goroutine 2 speichert den gelesenen Wert in der globalen Variablen a (1) und überschreibt damit den Wert, den Goroutine 1 geschrieben hat.

Das Problem einer Wettlaufsituation tritt immer dann ein, wenn mehrere Goroutinen auf globale Zustände (Variablen) schreibend zugreifen. Ärgerlich ist bei einer Wettlaufsituation, dass das Problem nur manchmal auftritt, nämlich nur genau dann, der betroffene Programmabschnitt sich in mehreren Goroutinen überlagern.

Der Go-Compilers hilft er beim Entdecken von race-conditions. Startet man das Programm mit der Option -race, präpariert der Compiler das Programm mit speziellen Instruktionen zum Auffinden problematischer Stellen. Wird das Beispielprogramm mit go run -race main.go gestartet, ergibt sich folgende Ausgabe:

$ go run -race main.go
==================
WARNING: DATA RACE
Write by goroutine 5:
  main.macheEtwas()
      /Users/patrick/prog/go/hello/main.go:12 +0x36
  gosched0()
      /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f

Previous write by goroutine 4:
  main.macheEtwas()
      /Users/patrick/prog/go/hello/main.go:12 +0x36
  gosched0()
      /usr/local/go/src/pkg/runtime/proc.c:1218 +0x9f

Goroutine 5 (running) created at:
  main.main()
      /Users/patrick/prog/go/hello/main.go:20 +0xdf
  runtime.main()
      /usr/local/go/src/pkg/runtime/proc.c:182 +0x91

Goroutine 4 (finished) created at:
  main.main()
      /Users/patrick/prog/go/hello/main.go:19 +0xc8
  runtime.main()
      /usr/local/go/src/pkg/runtime/proc.c:182 +0x91

==================
Fertig
a = 4
Found 1 data race(s)
exit status 66

Die Aussage ist klar: Found 1 data race(s) und der Hinweis auf die Zeile 12 (das ist die Zeile mit der Anweisung a = a + 1) sollte beim geübten Programmierer das Alarm-Lämpchen leuchten lassen.

Es muss also sichergestellt werden, dass das Lesen und Schreiben der globalen Variablen nur exklusiv in einer Goroutine gleichzeitig stattfinden darf. Um die Synchronisation zwischen Goroutinen zu erleichtern, stellt Go im Paket sync sogenannte »Mutex« (engl. mutual exclusion, gegenseitiger Ausschluss) bereit. Die Idee dahinter ist, vor einem kritischen Abschnitt mithilfe der Mutex eine Exklusivität zu erreichen. Keine andere Goroutine darf diesen kritischen Abschnitt dann betreten, wurde er nicht wieder von der ursprünglichen Goroutine freigegeben. Im Beispiel oben wäre das:

mtx.Lock()
a = a + 1
mtx.Unlock()

mtx ist eine Instanz vom Typ sync.Mutex. Sobald eine Goroutine mtx.Lock() aufruft, müssen alle anderen Goroutinen warten, bis mtx.Unlock() aufgerufen wird. Dann darf wieder nur eine Goroutine in den Abschnitt eintreten u.s.w. Das vollständige Beispiel ist:

package main

import (
    "fmt"
    "sync"
)

var a int

func macheEtwas(wg *sync.WaitGroup, mtx *sync.Mutex) {
    mtx.Lock()
    a = a + 1
    mtx.Unlock()
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    var mutex sync.Mutex
    wg.Add(4)
    go macheEtwas(&wg, &mutex)
    go macheEtwas(&wg, &mutex)
    go macheEtwas(&wg, &mutex)
    go macheEtwas(&wg, &mutex)

    wg.Wait()
    fmt.Println("Fertig")
    fmt.Println("a =", a)
}

Wir können überprüfen, dass keine race-condition vorliegt, in dem wir wieder go run -race main.go aufrufen. Dieses Mal wird keine Warnung ausgegeben.

Sichtbarkeit von Variablen in Goroutinen

Strenggenommen ist das kein Problem von Goroutinen, hier tritt es nur häufiger auf. Folgendes Beispiel:

package main

import (
	"fmt"
	"time"
)

func main() {
	data := []string{"eins", "zwei", "drei"}

	for _, v := range data {
		go func() {
			fmt.Println(v)
		}()
	}

	time.Sleep(3 * time.Second)
	//Goroutine schreibt: drei, drei, drei
}

Die Variable v verändert sich während der range-Schleife. Es ist nicht definiert, wann die Goroutine ausgeführt wird und damit nicht, welchen Wert v zu diesem Zeitpunkt hat.

Das Problem kann man auch mit go vet überprüfen. Das ist ein Tool, mit dem man Go-Programme auf Probleme untersuchen kann. Die Ausgabe von go vet main.go ist:

./main.go:13:16: loop variable v captured by func literal

Eine Lösung des Problems ist Variable der Funktion in der Goroutine zu übergeben:

package main

import (
	"fmt"
	"time"
)

func main() {
	data := []string{"eins", "zwei", "drei"}

	for _, v := range data {
		go func(s string) {
			fmt.Println(s)
		}(v)
	}

	time.Sleep(3 * time.Second)
	//Goroutine schreibt: eins, zwei, drei
}

Damit gibt auch go vet main.go keine Warnungen mehr aus.

Goroutinen beenden

Goroutinen lassen sich nicht einfach von außen steuern und damit beenden. Es gibt keine zentrale Verwaltung der Goroutinen, an die man ein Signal schicken kann. Mithilfe von Kanälen (siehe Kanäle (Channels)) lässt sich eine solche Steuerung simulieren. Entweder öffnet man einen eigenen Kanal, der nur das »beende dich« darstellt, oder man schließt einen Kanal und signalisiert der Goroutine damit das Ende der Verarbeitung. Ich zeige hier zwei Beispiele dafür:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)

	quit := make(chan bool)
	go func() {
		defer wg.Done()
		for {
			select {
			// wenn etwas aus dem Kanal »quit« kommt, dann...
			case <-quit:
				fmt.Println("Tschüss")
				return
			default:
				// ansonsten...
				fmt.Println("Hallo aus der Goroutine")
				time.Sleep(100 * time.Millisecond)
			}
		}
	}()
	fmt.Println("Hallo aus der Hauptschleife")
	// Anstelle von »schlafen« sollte hier echte Arbeit geleistet werden.
	time.Sleep(1 * time.Second)

	// Sende irgend einen Wert über den bool-channel mit dem Namen »quit«
	quit <- true

	wg.Wait()
}
Die Goroutine beendet sich, wenn über den Kanal »quit« ein Wert kommt. Dabei ist der Wert egal, es wird in der select-Schleife nur geprüft, ob etwas kommt.

Die Alternative zu einem eigenen Kanal ist es, den Kommunikationskanal einfach zu schließen.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)

	ch := make(chan int)
	go func() {
		defer wg.Done()
		// Die range-Schleife beendet sich,
		// wenn der Kanal geschlossen ist.
		for val := range ch {
			fmt.Println(val)
		}
		// Hiermit ist die Goroutine zu Ende
	}()

	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)

	wg.Wait()
}
Hier beendet sich die Goroutine, wenn der Kanal geschlossen ist. Die for-range-Schleife beendet sich wenn keine Daten mehr kommen können.

1. Diese Verbindungen sollen nur gleichzeitig aussehen, ob die wirklich im physikalischen Sinne gleichzeitig ausgeführt werden, ist für den Anwender meist unerheblich.
2. Ein Scheduler im Betriebssystem verteilt die laufenden Programme auf einen oder mehrere Prozessoren und gewährt ihnen für kurze Zeit den Zugriff auf den Prozessor. Anschließend kommt das nächste Programm an die Reihe und so fort. Das geht so schnell, dass der Anwender das Gefühl hat, dass die Prozesse gleichzeitig laufen. Go hat für die eigenen Goroutinen einen eigenen Scheduler eingebaut.