Go
Konkurencja
Szukaj…
Wprowadzenie
W Go współbieżność osiąga się poprzez użycie goroutyn, a komunikacja między goroutynami odbywa się zwykle za pomocą kanałów. Dostępne są jednak inne środki synchronizacji, takie jak muteksy i grupy oczekiwania, i należy ich używać, gdy są wygodniejsze niż kanały.
Składnia
- go doWork () // uruchom funkcję doWork jako goroutine
- ch: = make (chan int) // zadeklaruj nowy kanał typu int
- ch <- 1 // wysyłanie na kanał
- wartość = <-ch // odbieranie z kanału
Uwagi
Goroutines in Go są podobne do wątków w innych językach pod względem użytkowania. Wewnętrznie Go tworzy wiele wątków (określonych przez GOMAXPROCS
), a następnie planuje uruchomienie goroutine na wątkach. Z powodu tej konstrukcji mechanizmy współbieżności Go są znacznie wydajniejsze niż wątki pod względem zużycia pamięci i czasu inicjalizacji.
Tworzenie goroutines
Dowolną funkcję można wywołać jako goroutine, poprzedzając ją wywołaniem słowem kluczowym go
:
func DoMultiply(x,y int) {
// Simulate some hard work
time.Sleep(time.Second * 1)
fmt.Printf("Result: %d\n", x * y)
}
go DoMultiply(1,2) // first execution, non-blocking
go DoMultiply(3,4) // second execution, also non-blocking
// Results are printed after a single second only,
// not 2 seconds because they execute concurrently:
// Result: 2
// Result: 12
Zauważ, że zwracana wartość funkcji jest ignorowana.
Hello World Goroutine
pojedynczy kanał, jeden goroutine, jeden zapis, jeden odczyt.
package main
import "fmt"
import "time"
func main() {
// create new channel of type string
ch := make(chan string)
// start new anonymous goroutine
go func() {
time.Sleep(time.Second)
// send "Hello World" to channel
ch <- "Hello World"
}()
// read from channel
msg, ok := <-ch
fmt.Printf("msg='%s', ok='%v'\n", msg, ok)
}
Kanał ch
jest niebuforowanym lub synchronicznym kanałem .
time.Sleep
jest tutaj, aby zilustrować funkcję main()
która zaczeka na kanale ch
, co oznacza, że funkcja dosłownie wykonana jako goroutine ma czas na przesłanie wartości przez ten kanał: operator odbierania <-ch
zablokuje wykonanie main()
. Gdyby tak nie było, goroutine zostałby zabity, gdy main()
wyjdzie, i nie miałby czasu na przesłanie swojej wartości.
Czekam na goroutiny
Programy Go kończą się, gdy kończy się main
funkcja , dlatego powszechną praktyką jest czekanie na zakończenie wszystkich goroutyn. Częstym rozwiązaniem tego problemu jest użycie obiektu sync.WaitGroup .
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 1
func routine(i int) {
defer wg.Done() // 3
fmt.Printf("routine %v finished\n", i)
}
func main() {
wg.Add(10) // 2
for i := 0; i < 10; i++ {
go routine(i) // *
}
wg.Wait() // 4
fmt.Println("main finished")
}
Uruchom przykład na placu zabaw
Użycie WaitGroup w kolejności wykonania:
- Deklaracja zmiennej globalnej. Uczynienie go globalnym jest najłatwiejszym sposobem, aby był widoczny dla wszystkich funkcji i metod.
- Zwiększenie licznika. Należy to zrobić w głównym goroutine, ponieważ nie ma gwarancji, że nowo uruchomiony goroutine wykona się przed 4 ze względu na gwarancje modelu pamięci.
- Zmniejszenie licznika. Trzeba to zrobić przy wyjściu z goroutine. Używając odroczonego wywołania, upewniamy się, że będzie ono wywoływane za każdym razem, gdy funkcja się zakończy , bez względu na to, jak się zakończy.
- Oczekiwanie, aż licznik osiągnie 0. Trzeba to zrobić w głównym goroutine, aby zapobiec wyjściu programu przed zakończeniem działania wszystkich goroutine.
* Parametry są oceniane przed rozpoczęciem nowego goroutine . Dlatego konieczne jest jawne zdefiniowanie ich wartości przed wg.Add(10)
, aby ewentualnie kod panikujący nie zwiększył licznika. Dodanie 10 elementów do WaitGroup, więc będzie czekać na 10 elementów przed wg.Wait
zwraca kontrolę z powrotem do main()
goroutine. Tutaj wartość i jest zdefiniowana w pętli for.
Używanie zamknięć z goroutynami w pętli
W pętli zmienna pętli (val) w poniższym przykładzie jest pojedynczą zmienną, która zmienia wartość, gdy przechodzi przez pętlę. Dlatego należy wykonać następujące czynności, aby faktycznie przekazać każdą wartość wartości do goroutine:
for val := range values {
go func(val interface{}) {
fmt.Println(val)
}(val)
}
Jeśli miałbyś to zrobić, po prostu zrób func(val interface{}) { ... }()
bez przekazywania wartości val, wówczas wartość val
będzie równa wartości val, gdy faktycznie uruchomią się goroutines.
Innym sposobem na uzyskanie tego samego efektu jest:
for val := range values {
val := val
go func() {
fmt.Println(val)
}()
}
Dziwnie wyglądająca wartość val := val
tworzy nową zmienną w każdej iteracji, do której następnie ma dostęp goroutine.
Zatrzymywanie goroutines
package main
import (
"log"
"sync"
"time"
)
func main() {
// The WaitGroup lets the main goroutine wait for all other goroutines
// to terminate. However, this is no implicit in Go. The WaitGroup must
// be explicitely incremented prior to the execution of any goroutine
// (i.e. before the `go` keyword) and it must be decremented by calling
// wg.Done() at the end of every goroutine (typically via the `defer` keyword).
wg := sync.WaitGroup{}
// The stop channel is an unbuffered channel that is closed when the main
// thread wants all other goroutines to terminate (there is no way to
// interrupt another goroutine in Go). Each goroutine must multiplex its
// work with the stop channel to guarantee liveness.
stopCh := make(chan struct{})
for i := 0; i < 5; i++ {
// It is important that the WaitGroup is incremented before we start
// the goroutine (and not within the goroutine) because the scheduler
// makes no guarantee that the goroutine starts execution prior to
// the main goroutine calling wg.Wait().
wg.Add(1)
go func(i int, stopCh <-chan struct{}) {
// The defer keyword guarantees that the WaitGroup count is
// decremented when the goroutine exits.
defer wg.Done()
log.Printf("started goroutine %d", i)
select {
// Since we never send empty structs on this channel we can
// take the return of a receive on the channel to mean that the
// channel has been closed (recall that receive never blocks on
// closed channels).
case <-stopCh:
log.Printf("stopped goroutine %d", i)
}
}(i, stopCh)
}
time.Sleep(time.Second * 5)
close(stopCh)
log.Printf("stopping goroutines")
wg.Wait()
log.Printf("all goroutines stopped")
}
Ping pong z dwoma goroutinami
package main
import (
"fmt"
"time"
)
// The pinger prints a ping and waits for a pong
func pinger(pinger <-chan int, ponger chan<- int) {
for {
<-pinger
fmt.Println("ping")
time.Sleep(time.Second)
ponger <- 1
}
}
// The ponger prints a pong and waits for a ping
func ponger(pinger chan<- int, ponger <-chan int) {
for {
<-ponger
fmt.Println("pong")
time.Sleep(time.Second)
pinger <- 1
}
}
func main() {
ping := make(chan int)
pong := make(chan int)
go pinger(ping, pong)
go ponger(ping, pong)
// The main goroutine starts the ping/pong by sending into the ping channel
ping <- 1
for {
// Block the main thread until an interrupt
time.Sleep(time.Second)
}
}
Uruchom nieco zmodyfikowaną wersję tego kodu w Go Playground