В предишния пример използвахме изрично заключване на данните с помощта на превключвачи, за да съгласуваме достъпа до споделено състояние между множество гозадачи. Друга възможност да постигнем същото е да използваме вградените възможности за съгласуване в езика чрез гозадачи и канали. Този подход, основан на канали е в съзвучие с представата на Го да се споделя памет чрез предаване на съобщения между гозадачите и едно парче данни да се владее винаги само от една гозадача.
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
В този пример състоянието на данните ще се владее само
от една единствена гозадача. Така данните няма как да бъдат
повредени при едновременен достъп, просто защото няма
такъв. За да четат или променят това състояние, другите
гозадачи ще изпращат съобщения на владеещата
състоянието на данните гозадача и ще получават
съответни отговори от нея.
Тези стртури – четене и писане съдържат в себе си
самите заявки и начини за владеещата състоянието
гозадача да отговаря. Начините са каналите, именувани
отговор.
type четене struct {
key int
отговор chan int
}
type писане struct {
key int
val int
отговор chan bool
}
func main() {
Както преди, ще броим извършените действия.
var бройЧетения uint64
var бройПисания uint64
Каналите четения и писания ще се използват от
другите гозадачи, за да правят заявки за четене и
писане.
четения := make(chan четене)
писания := make(chan писане)
Ето я задачата, която притежава състоянието.
Състоянието е карта, както в предния пример, но
сега е частна собственост на състоятелната
гозадача. Тази гозадача непрекъснато избира чрез
select пристигащи по каналите четения и
писания стойности като веднага отговаря на
заявките щом пристигнат. Отговорът бива изпратен по
съответния за действието канал, веднага след като
бъде изпълнено заявеното действие. Самото изпращане
по съответния канал за отговор означава, че
действието е успешно. В случай на заявка за четене
бива изпратена заявената стойност, а в случай на
заявка за писане – двоична стойност – „да”
(true).
go func() {
var състояние = make(map[int]int)
for {
select {
case read := <-четения:
read.отговор <- състояние[read.key]
case write := <-писания:
състояние[write.key] = write.val
write.отговор <- true
}
}
}()
Това повторение пуска 100 гозадачи, да правят
заявки за четене към владеещата състоянието
гозадачача по канала четения. Всяко четене
изисква да се напраи заявка за четене, като се
състави ново четене и се изпрати по канала
четения, а отговорът бива получен по канала
read.отговор, като напишем <-read.отговор.
for range 100 {
go func() {
for {
read := четене{
key: rand.Intn(5),
отговор: make(chan int)}
четения <- read
<-read.отговор
atomic.AddUint64(&бройЧетения, 1)
time.Sleep(time.Millisecond)
}
}()
}
По подобен начин пускаме и 10 писания.
for range 10 {
go func() {
for {
write := писане{
key: rand.Intn(5),
val: rand.Intn(100),
отговор: make(chan bool)}
писания <- write
<-write.отговор
atomic.AddUint64(&бройПисания, 1)
time.Sleep(time.Millisecond)
}
}()
}
Да оставим гозадачите да поработят една секунда.
time.Sleep(time.Second)
Накрая извеждаме отчет за броя, извършени действия.
краенБройЧетения := atomic.LoadUint64(&бройЧетения)
fmt.Println("Общо четения:", краенБройЧетения)
краенБройПисания := atomic.LoadUint64(&бройПисания)
fmt.Println("Общо писания:", краенБройПисания)
}
Като пуснем програмата виждаме, че при основаното на гозадачи управление на състоянието, тя извършва (на моето сметало) общо 73732 действия.
$ go run stateful-goroutines.go
Общо четения: 66790
Общо писания: 6942
Точно в този случай подходът, основан на гозадачи беше малко по-сложен от основаващия се на превключватели. Все пак може да е полезно в някои случаи. Например когато работите с други канали или когато използването на превключватели, може да доведе до повече грешки. Използвайте подхода, който ви изглежда по-естествен, особено с оглед на по-лесното разбиране, дали програмата ви работи правилно.
Следващ пример: Подреждане.