Concurrency
Die Nebenläufigkeit, welche in Go als Sprachkonstrukte vorliegen, listet Paul Krill (2015) als wichtigen Vorteil dieser Programmiersprache auf. Das Mantra von Leyden (2014) lautet:
Don’t communicate by sharing memory, share memory by communicating.
Goroutines
Eine Goroutine ist ein leichtgewichtiger Ausführungsstrang. Leyden (2014) erwähnt die Unterschiede zwischen Goroutinen und Threads. Wobei erwähnt wird, dass Java bspw. die Threads direkt zu OS Threads mappt. Das Problem daran ist, dass diese Threads eine grosse und fixe Stack Size besitzen. In Go sind diese segmentiert und wachsen nur, wenn sie benötigt werden. Die Go Runtime scheduled und nicht das OS.
Jede Funktion in Go kann nebenläufig aufgerufen werden. Der Aufrufer entscheidet darüber mit dem Keyword go
. Im Code-Beispiel sehen wir, dass printMe
zuerst synchron aufgerufen wird, gefolgt von zwei asynchronen Calls. Die Sprache selbst bietet somit ein Konstrukt an um alles asynchron aufrufen zu können.
func main() {
printMe("Synchroner Peter", 5)
go printMe("Asynchroner Markus", 5)
go printMe("Asynchrone Anna", 5)
printMe("Synchoner Fabian", 5)
var input string
fmt.Scanln(&input)
}
func printMe(text string, times int) {
for i := 0; i < times; i++ {
time.Sleep(1000)
fmt.Println(text, ":", i)
}
}
Channels
Channels dienen der Kommunikation zwischen Goroutinen. Man kann sich das wie folgt vorstellen. Ich sende einen Wert in den Channel aus einer Goroutine und kann diesen in einer anderen Goroutine empfangen. Channels sind typensicher. Falls ich Strings senden und empfangen will, brauche ich dazu einen Channel typisiert auf String.
Channel erzeugen
fruits := make(chan string)
Werte in den Channel senden
fruits <- "Apple"
Werte aus Channel empfangen
fruit := <-fruits
Sowohl senden wie auch empfangen sind blockierend. Es kann erst gesendet werden, wenn es dafür einen Empfänger gibt. Der Vorteil: Wir müssen keine weiteren synchronisierende Elemente implementieren. Die Daten sind nur an einem Ort verfügbar.
func main() {
fruits := make(chan string)
go func() {
fruits <- "Apple"
time.Sleep(3 * time.Second)
fruits <- "Banana"
fruits <- "Orange"
} ()
fruit := <-fruits
fmt.Println(fruit)
fruit = <-fruits
fmt.Println(fruit)
time.Sleep(3 * time.Second)
fruit = <-fruits
fmt.Println(fruit)
}
Channel-Buffering
Channels buffern ohne explizite Definition nie. In diesem Fall kann erst ein Wert in den Channel gesendet werden, wenn es einen Empfänger dafür gibt. Das Channel-Buffering ermöglicht es, dass Werte in den Channel geschrieben werden und diese erst zu einem späteren Zeitpunkt empfangen werden. Die Anzahl Werte im Buffer muss definiert werden.
Im folgenden Code-Beispiel wird ein Channel mit der Buffergrösse von 2
erzeugt.
fruits := make(chan string, 2)
Der Channel blockiert beim Schreiben, sobald dieser voll ist. Nachdem ein Wert empfangen wird, können wieder neue Werte in den Channel geschrieben werden.
func main() {
fruits := make(chan string, 2)
go func() {
fruits <- "Apple"
fmt.Println("Apple im Channel")
fruits <- "Banana"
fmt.Println("Banana im Channel")
fruits <- "Orange"
fmt.Println("Orange im Channel")
} ()
time.Sleep(3 * time.Second)
fruit := <-fruits
fmt.Println(fruit)
time.Sleep(3 * time.Second)
fruit = <-fruits
fmt.Println(fruit)
time.Sleep(3 * time.Second)
fruit = <-fruits
fmt.Println(fruit)
}
Channel-Synchronization
Um Goroutinen zu synchronisieren, können Channels verwendet werden. Der Empfang aus einem Channel ist immer blockierend, dies kann man sich zu Nutze machen.
Im folgenden Code-Beispiel wird ein eigener Channel namens workIsDone
typisiert auf bool
dafür erzeugt. Die Funktion, welche asynchron aufgerufen wurde, sendet nach Beendigung der Arbeit ein true
in den Channel.
Der Empfang <- workIsDone
ist blockierend. Der Inhalt wird keiner Variabeln zugewiesen und wird verworfen.
func main() {
workIsDone := make(chan bool)
go doSomeWork(workIsDone)
fmt.Println("Warten auf das Ende von doSomeWork")
<- workIsDone
fmt.Println("doSomeWork ist beendet")
}
func doSomeWork(workIsDone chan bool) {
fmt.Println("do work 1")
time.Sleep(2 * time.Second)
fmt.Println("do work 2")
workIsDone <- true
}
Channel-Directions
Falls Channels als Funktions-Parameter übergeben werden, kann definiert werden ob auf den Channel nur gesendet oder auch empfangen werden darf. Wird in der Implementation der Methode diese Definition missachtet, kommt es zu einem Fehler zur Kompilierzeit.
Hier darf nur gesendet werden:
func produce(fruits chan<- string) {
fruits <- "Apple"
}
Hier darf nur empfangen werden:
func consume(fruits <-chan string) {
fruit := <- fruits
}
Select
Mittels select
kann gleichzeitig auf mehreren Channels gehorcht werden.
Im Beispiel gibt es einen Apple-Channel, welcher pro Sekunde ein Apfel erhält. Daneben existiert ein Banana-Channel, welcher all zwei Sekunden gefüttert wird.
Das select
Statement ermöglicht es uns auf mehreren Channels gleichzeitig zu warten. Sobald ein Channel Daten enthält, wird dieser verarbeitet.
func main() {
chanApple := make(chan string)
chanBanana := make(chan string)
go func() {
for {
time.Sleep(time.Second * 1)
chanApple <- "Apple"
}
}()
go func() {
for {
time.Sleep(time.Second * 2)
chanBanana <- "Banana"
}
}()
for {
select {
case apple := <-chanApple:
fmt.Println("Fruit: ", apple)
case banana := <-chanBanana:
fmt.Println("Fruit: ", banana)
}
}
}
Timeouts
Das Select-Statement kann auch Timeout-Clauses enthalten. Sofern das Select zwei Sekunden keine Früchte erhält, kommt der Timeout-Clause zum Zug.
for {
select {
case apple := <-chanApple:
fmt.Println("Fruit: ", apple)
case banana := <-chanBanana:
fmt.Println("Fruit: ", banana)
case <- time.After(time.Second * 2):
fmt.Println("Ich habe 2 Sekunden gewartet und nichts erhalten.")
}
}
Non-Blocking Channel Operations
Ergänzt man das Select um einen Default-Case, können nicht blockierende Channel Operationen durchgeführt werden.
Falls kein Empfänger vorhanden ist, wird der apple nicht gesendet.
select {
case chanApple <- "apple":
fmt.Println("Apple sent")
default:
fmt.Println("Nothing sent")
}
Dasselbe ist mit dem Empfangen möglich. Sofern kein Wert im Channel ist, kommt der Default-Case zum Zug.
select {
case apple := <-chanApple:
fmt.Println("Apple received: ", apple)
default:
fmt.Println("Nothing received")
}
Closing Channels
Channels können geschlossen werden. Damit wird signalisiert, dass keine weiteren Werte zu erwarten sind.
Mittels close(fruits)
wird der Channel geschlossen. Wenn Werte empfangen werden, muss nun auch abgefragt werden ob der Channel zu ist. Dafür bietet das Empfangen von Werten aus einem Channel das Konstrukt der Multiple Return Values. Open signalisiert ob der Channel weiterhin offen ist:
fruit, open := <-fruits
func main() {
fruits := make(chan string)
done := make(chan bool)
go func() {
for {
fruit, open := <-fruits
if (open) {
fmt.Println("Fruit: ", fruit)
} else {
fmt.Println("no more fruits")
done <- true
return
}
}
}()
fruits <- "Apple"
fruits <- "Banana"
close(fruits)
<-done
}
Timer & Tickers
Wenn Aktionen in Zukunft (Timer) oder zeitlich repetiv (Ticker) ausgeführt werden müssen bietet Go bereits Konstrukte an. Diese Arbeiten mit Channels. Man definieren einen Timer
timer := time.NewTimer(time.Second * 2)
An einem beliebigen Punkt kann gewartet werden, bis der Timer abgelaufen ist.
<- timer.C
Warum wird nicht einfach time.Sleep()
verwendet? Timer können mittels stop()
gestoppt werden.
Wenn Aktionen zeitlich repetive ausgeführt werden müssen, kann ein Ticker definiert werden.
ticker := time.NewTicker(time.Millisecond * 500)
Der Ticker bedient sich auch dem Channel und kann mittels range over channel iteriert werden.
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
Worker Pools
Worker-Pools können einfach implementiert werden. Dafür werden keine weiteren Konstrukte benötigt.
In unserem Beispiel gibt es Pflücker (picker), welche Früchte sammeln. Und zum anderen Esser (eater), welche Früchte essen.
Der Picker pflückt in einem bestimmten Intervall Früchte und schreibt diese in den Channel.
func picker(fruit string, fruits chan<- string, timeToPick int) {
for {
fmt.Println("Pick ", fruit)
fruits <- fruit
time.Sleep(time.Duration(timeToPick) * time.Second)
}
}
Der Esser isst die Frucht.
func eater(i int, fruits <-chan string) {
for fruit := range fruits {
fmt.Println("Esser ", i, "Mhmmm ", fruit)
time.Sleep(time.Second * 8)
}
}
Es werden 10 Esser erzeugt und 3 Pflücker.
func main() {
fruits := make(chan string, 1000)
for i := 1; i <= 10; i++ {
time.Sleep(time.Millisecond * 600)
go eater(i, fruits)
}
go picker("Apple", fruits, 5)
time.Sleep(time.Millisecond * 600)
go picker("Orange", fruits, 3)
time.Sleep(time.Millisecond * 600)
go picker("Strawberry", fruits, 2)
time.Sleep(time.Second * 360)
}
Das Programm arbeitet nun total asnychron und die Kommunikation der einzelnen Teile findet über Channel statt.
Krill, Paul (2015), Google's Go language is off to a great start, but still has work ahead, Gefunden unter: http://www.infoworld.com/article/2896575/google-go/googles-go-language-pros-and-cons.html
Leyden, Traul (2014), Goroutines vs Threads, Gefunden unter:: http://tleyden.github.io/blog/2014/10/30/goroutines-vs-threads/