Golang: Threading mit Hilfe von goroutines

Mittels Goroutines lassen sich in Go leichtgewichtige Threads starten. Unter Verwendung des go Statements lassen sich Funktionen, Anonyme-Funktionen oder Methoden als Goroutine ausführen. Die Main-Function eines Go-Programms kann auch als Goroutine betrachtet werden. Dabei gilt zu beachten, dass keine Goroutines mehr ausgeführt werden, sobald der Main-Thread beendet wurde. Verdeutlichen lässt sich das am folgenden Beispiel, bei welchem die Main-Funktion fertig ist, bevor die doWork-Routine ausgeführt werden konnte. Somit erscheint in der Ausgabe nur „Done some work“.

package main

import "fmt"

func doWork() {
    fmt.Println("Do work")
}

func main() {
    go doWork()
    fmt.Println("Done some work")
}

Ausgabe:

Done some work

Um der doWork-Routine genug Zeit einzuräumen, damit sie ausgeführt werden kann, könnte mittels time.sleep in der main-Funktion eine künstliche Verzögerung eingebaut werden. Allerdings bietet Go einen besseren Weg an. Mit Hilfe von WaitGroups können mehrere Goroutinen mitteilen, wann sie mit ihrer Arbeit fertig sind. Das nachfolgende Beispiel wurde um WaitGroups erweitert und ermöglicht somit, dass die Goroutine ausgeführt werden kann. Dabei gilt zu beachten, dass die WaitGroup als Pointer an die Funktion übergeben werden muss. Durch den Aufruf von wg.Wait() wird die weitere Ausführung in der Main-Funktion so lange blockiert, bis alle Goroutinen durch den Aufruf wg.Done() mitgeteilt haben, dass sie mit ihrer Arbeit fertig sind.

package main

import (
    "fmt"
    "sync"
)

func doWork(wg *sync.WaitGroup) {
    fmt.Println("Doing some work")
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go doWork(&wg)
    wg.Wait()

    fmt.Println("Done some work")
}

Ausgabe:

Doing some work
Done some work

GOMAXPROCS

Um die Anzahl der CPUs zu setzen, auf welchen Goroutinen Parallel ausgeführt werden können, bietet das runtime-Package von Golang die Funktion func GOMAXPROCS(n int) int an:

GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously

Ebenfalls ist der Dokumentation zu entnehmen, dass dieser Aufruf entfernt wird, sobald sich der Scheduler verbessert. Standardmäßig wird der Wert für GOMAXPROCS auf die Anzahl der zur Verfügung stehenden Cores zur Ausführung von Go-Programmen gesetzt.

Beispiel Anwendung

Die beiden oben gezeigten Beispiele sind etwas abstrakt. Nachfolgend wird ein Szenario gezeigt, bei welchem URLs geladen werden sollen. Um den Vorteil in der Ausführungszeit zu messen, führen wir das Beispiel einmal ohne und einmal mit Goroutinen aus.

package main

import (
    "fmt"
    "net/http"
    "time"
)

var urls = []string{
    "https://google.com",
    "https://news.ycombinator.com",
    "https://www.heise.de",
}

func fetch(url string) {
    _, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Printf("Fetched url %v\n", url)
}

func main() {
    start := time.Now()
    for _, url := range urls {
        fmt.Printf("Fetching url %v\n", url)
        fetch(url)
    }
	
    diff := time.Since(start)
    fmt.Printf("Diff %s\n", diff)
}

Ausgabe:

Fetching url https://google.com
Fetched url https://google.com
Fetching url https://news.ycombinator.com
Fetched url https://news.ycombinator.com
Fetching url https://www.heise.de
Fetched url https://www.heise.de
Diff 1.247790261s

Wie der Ausgabe zu entnehmen ist, werden die URLs entsprechend der Reihenfolge, wie sie im Array stehen, geladen. Der gesamte Prozess dauert dabei etwas über eine Sekunde. Im nachfolgenden Beispiel werden die gleichen URLs geladen, allerdings gleichzeitig unter Einsatz von Goroutinen und WaitGroups.

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

var urls = []string{
    "https://google.com",
    "https://news.ycombinator.com",
    "https://www.heise.de",
}

func fetch(url string, wg *sync.WaitGroup) {
    _, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Printf("Fetched url %v\n", url)
    wg.Done()
}

func main() {
    start := time.Now()

    var wg sync.WaitGroup
    for _, url := range urls {
        wg.Add(1)
        fmt.Printf("Fetching url %v\n", url)
        go fetch(url, &wg)
    }
    wg.Wait()

    diff := time.Since(start)
    fmt.Printf("Diff %s\n", diff)
}

Ausgabe:

Fetching url https://google.com
Fetching url https://news.ycombinator.com
Fetching url https://www.heise.de
Fetched url https://www.heise.de
Fetched url https://google.com
Fetched url https://news.ycombinator.com
Diff 677.755971ms

Wie zu sehen ist, werden die URLs jetzt nicht mehr der Reihenfolge nach geladen. Auch die Ausführungszeit liegt nun deutlich unter einer Sekunde.

Show Comments