Język Go pozwala na łatwe i szybkie implementowanie fragmentów kodu asynchronicznie. Podstawą działania jest wykorzystanie słowa kluczowego „go” oraz kanałów. Kanały służą do komunikacji między współbieżnymi aktywnościami programu, nazywanymi w kontekście języka Go funkcjami goroutine.
Każdy uruchomiony w Go program posiada goroutine główną, czyli tę, która wywołała funkcję main(). Kolejne zaś można stworzyć przez dodanie przed dowolną funkcją słowa kluczowego „go”.
Domyślnie język Go może wykorzystać do aktywnego wykonywania kodu tyle wątków systemu operacyjnego, ile wynosi liczba procesorów maszyny.
Ciekawostka
Środowisko wykonawcze języka Go posiada własnego planistę, którego zadania są analogiczne do zadań planisty jądra systemu operacyjnego, ale zajmuje się on tylko goroutine pojedynczego programu Go.
Napisałam prostą funkcję call(), która symuluje pewne czasochłonne zadanie, np. oczekiwanie na odpowiedź serwera. Generuje ona liczbę losową typu int z przedziału <0; 350). Następnie wykonuje na niej operacje przekształcające jej typ na time.Duration, który reprezentuje przedział czasowy (tutaj wyrażony w milisekundach) i, zanim zwróci tę wartość, czeka wygenerowaną liczbę milisekund.
Dzięki wywołaniu rand.Seed() na samym początku main() program będzie za każdym uruchomieniem generował inne liczby losowe.
func main() {
rand.Seed(time.Now().UnixNano())
}
func call() time.Duration {
resp_time := time.Duration(rand.Intn(350)) * time.Millisecond
time.Sleep(resp_time)
return resp_time
}
Funkcja main() wywołuje w pętli funkcję call() n razy. Na końcu program wypisuje do standardowego wyjścia sumę wartości zwracanych przez funkcję call() przechowaną w zmiennej linear_time oraz rzeczywisty czas wykonania programu (zmienna real_time).
Na razie te czasy są niemal identycznie, ponieważ kod wykonuje się synchronicznie.
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
const n = 10
var linear_time time.Duration = 0
for i := 0; i < n; i++ {
linear_time += call()
}
real_time := time.Since(start)
fmt.Println("Linear time:\t",linear_time)
fmt.Println("Real time:\t", real_time.String())
}
Poniżej całość kodu synchronicznego:
package main
import (
"time"
"math/rand"
"fmt"
)
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
const n = 10
var linear_time time.Duration = 0
for i := 0; i < n; i++ {
linear_time += call()
}
real_time := time.Since(start)
fmt.Println("Linear time:\t",linear_time)
fmt.Println("Real time:\t", real_time.String())
}
func call() time.Duration {
resp_time := time.Duration(rand.Intn(350)) * time.Millisecond
time.Sleep(resp_time)
return resp_time
}
Przykładowy wydruk ze standardowego wyjścia:
Pora wykorzystać polecenie „go” i utworzyć kanał, który umożliwi komunikację między goroutine’ami main() i call().
Nazwę kanał resp_time i będę w nim przechowywać wartości zwracane przez funkcję call(), czyli zmienne typu time.Duration.
Resp_time przekażę do funkcji call(), którą wywołam w pętli asynchronicznie, dodając słowo kluczowe „go”.
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
const n = 10
var linear_time time.Duration = 0
resp_time := make(chan time.Duration)
for i := 0; i < n; i++ {
go call(resp_time)
}
real_time := time.Since(start)
fmt.Println("Real time:\t", real_time.String())
fmt.Println("Linear time:\t",linear_time)
}
func call(resp_time chan time.Duration) {
duration := time.Duration(rand.Intn(350)) * time.Millisecond
time.Sleep(duration)
resp_time <- duration
}
W powyższym kodzie brakuje odczytu danych z kanału, dlatego dane uciekają w eter, a funkcja main() nawet nie czeka na powrót n wywołań funkcji call().
Dla odmiany, utworzę funkcję odczytującą dane z kanału jako anonimową. Ją także wywołam jako osobną goroutine, tak jak wcześniej funkcję call().
for i := 0; i < n; i++ {
go func(n time.Duration) {
linear_time += n
}(<-resp_time)
}
Poniżej całość kodu asynchronicznego:
package main
import (
"time"
"math/rand"
"fmt"
)
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
const n = 10
var linear_time time.Duration = 0
resp_time := make(chan time.Duration)
for i := 0; i < n; i++ {
go call(resp_time)
}
for i := 0; i < n; i++ {
go func(n time.Duration) {
// fmt.Println("reading ",n)
linear_time += n
}(<-resp_time)
}
real_time := time.Since(start)
fmt.Println("Real time:\t", real_time.String())
fmt.Println("Linear time:\t",linear_time)
}
func call(resp_time chan time.Duration) {
duration := time.Duration(rand.Intn(350)) * time.Millisecond
// fmt.Println("writening",duration)
time.Sleep(duration)
resp_time <- duration
}
Dzięki zdjęciu komentarza z poleceń: fmt.Println(„reading „,n) oraz fmt.Println(„writening”,duration) można zauważyć w wydruku ze standardowego wyjścia, że operacje zapisu do kanału i odczytu z kanału wykonują się – na pozór – w nieprzewidywalnej kolejności. Jeśli przyjrzysz się dokładniej, zauważysz, że odczyt następuje w kolejności od czasu najkrótszego do najdłuższego. Czas wykonania programu nie jest równy sumie wszystkich wykonań funkcji call(), ale najdłuższej spośród wykonań funkcji call(). W efekcie, program zamiast wykonywać pracę w czasie 1 sek, zakończył ją już po ok. 340ms.
Język Go ma wbudowany bardzo wydajny system pozwalający na tworzenie kodu asynchronicznego. Niewielkim nakładem pracy można znacząco przyspieszyć działanie programu przez wykonywanie jego fragmentów jednocześnie na kilku/wszystkich rdzeniach maszyny.
Do komunikacji pomiędzy goroutines można – zamiast kanałów – wykorzystać typy Once i WaitGroup z biblioteki ‚sync’. Zwiera ona także inne typy, ale te są przewidziane do synchronizacji goroutines na niższym poziomie. Zaleca się, aby wysokopoziomową synchronizację wykonywać jednak przy użyciu kanałów.
Autor: Paulina Sorys
Źródła
Komentarze (0)