Teil 4 - Concurrency
In den meisten Sprachen würde dieses Feature nicht zu den Basics gehören, wir haben uns aber hier ausdrücklich dafür entschieden.
Go ist durch seine Struktur und seine Funktionsweise prädestiniert dazu sehr komplexe Programme umzusetzen. Die nativ und gut durchdachte Umsetzung von concurrency ist eines der bedeutensten Features von GO überhaupt und damit für uns ein guter Grund auch hier schon die Grundlagen zu beschreiben.
Go Routines
Was man in anderen Sprachen als Threads kennt, nennt sich hier go routines. Dies sind im grunde sehr leichtgewichtige Threads die in der Anwendung gestartet werden.
Das Starten einer Go Routine ist sehr einfach:
go f(x)
Die Funktion f(x) wird nun im Hintergrund weiter ausgeführt und teilt sich den selben Adressbereich wie der main thread. Zu beachten ist aber, dass die Evaluation der Parameter von f() noch im aktuellen Kontext erfolgt und erst die eigentliche Funktion im Hintergrund ausgeführt wird.
Das bedeutet, bei:
go f(x, p(y), z)
wird die Funktion p(y) noch im aktuellen Kontext ausgeführt, nicht im Hintergrund.
Channels
Was bringt einem jedoch ein Thread, wenn man nicht damit kommunizieren kann. In C/C++ gibt es dafür etwas das sich Shared Memory nennt und gewisse Bereiche des Arbeitsspeichers mit allen Threads teilt. In Go ist dieses Konzept völlig unnötig (es wäre zu unperformant). Daher gibt es die sog. channels.
Ein Channel ermöglicht es, typisierte Werte zu senden bzw. zu empfangen. Die grundlegende Syntax ist folgende:
ch := make(chan int) // Channel vom Typ int erstellen
ch <- v // Senden von v an den Channel
v := <-ch // Empfangen vom Channel, und den Wert auf v zuweisen
Wichtig bei Channels ist allerdings, dass sowohl lesen als auch schreiben blockende Operationen sind. Das bedeutet, der Prozess wartet so lange, bis der Channel entweder etwas empfängt (bei reads) oder gelesen wurde (bei writes). Dadurch ist eine sehr einfache Synchronisation von go routines möglich.
Als alternative zu den standard Channels gibt es noch die sog. Buffered Channels die erst blocken, wenn der definierte Buffer voll ist:
ch := make(chan int, 2)
Es kann hier also 2x in den Channel gesendet werden, bevor die Operation blockiert. Genau so kann danach auch 2x vom Channel empfangen werden, bis dieser blockiert.
Channels können auch geschlossen werden, was aber in den meisten Fällen nicht nötig ist:
close(ch)
Das Schließen von Channels ist nur hilfreich, wenn der lesenden Funktion aktiv mitgeteilt werden soll, dass es keine weiteren Daten mehr geben wird. Das Schließen sollte immer durch die sendende Routine erfolgen, da ein senden auf einen geschlossenen Channel einen panic im Programm verursacht.
Ob ein Channel geschlossen ist, lässt sich einfach mit einem weiteren Parameter beim lesen überprüfen:
v, ok := <-ch
ok ist hierbei false wenn ch geschlossen wurde.
Zusätzlich gibt es mit range noch ein weiteres Feature, das wiederholt die Werte des Channels ließt bis dieser geschlossen wird:
for i := range ch {
fmt.Println(i)
}
Folgende Funktion gibt damit genau 10 Fibonacci Zahlen aus, bis der Channel dann geschlossen wird:
func fibonacci(n int, ch chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
ch <- x
x, y = y, x+y
}
close(ch)
}
func main() {
ch := make(chan int, 10)
go fibonacci(cap(ch), ch)
for i := range ch {
fmt.Println(i)
}
}
Mutex
Alle die schon Angst hatten, es gäbe das gute alte Mutex nicht in Go, hier die Erlösung.
In seiner standard Bibliothek liefert Go diverse sync Features wie auch das Mutex:
mux sync.Mutex
Damit kann einfach sichergestellt werden, dass immer nur ein Zugriff auf ein Objekt erfolgt:
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
v int
mux sync.Mutex
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc() {
c.mux.Lock()
// Lock damit nur eine Routine gleichzeitig Zugriff hat.
c.v++
c.mux.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value() int {
c.mux.Lock()
// Lock damit nur eine Routine gleichzeitig Zugriff hat.
defer c.mux.Unlock()
return c.v
}
Hier wird der SafeCounter mit einem Mutex versehen und dieses wird dann in den Funktionen gelockt um nur einen Zugriff gleichzeitig zu ermöglichen. In der Value Funktion sieht man wieder ein Beispiel für die Anwendung des defer's.
Mit diesen Grundlagen sind wir nun schon in der Lage viel zu erreichen.