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()
}
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:
-
Goroutine 1 liest den Inhalt der globalen Variable
a
(0) -
Goroutine 2 liest den Inhalt der globalen Variable
a
(0) - Goroutine 1 erhöht den gelesenen Wert um eins (1). Damit ist der gelesene Wert von Goroutine 2 nicht mehr sinnvoll.
- Goroutine 2 erhöht den gelesenen Wert um eins (1). Eigentlich sollte der Wert jetzt 2 sein, da Punkt 3 ausgeführt wurde.
-
Goroutine 1 speichert den gelesenen Wert in der globalen Variablen
a
(1) -
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:
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()
}
for
-range
-Schleife beendet sich wenn keine Daten mehr kommen können.