В предыдущем примере мы использовали явную блокировку с мьютексами для синхронизации доступа к общему состоянию между несколькими горутинами. Другой вариант - использовать встроенные функции синхронизации для горутин и каналов для достижения того же результата. Этот подход, основанный на каналах, согласуется с идеями Go о совместном использовании памяти путем обмена данными и владения каждой частью данных ровно 1 горутиной. |
|
package main
|
|
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
|
|
В этом примере наше состояние будет принадлежать
единственной горутине. Это гарантирует, что данные
никогда не будут повреждены при одновременном доступе.
Чтобы прочитать или записать это состояние, другие
горутины будут отправлять сообщения горутин-владельцу
и получать соответствующие ответы. Эти |
type readOp struct {
key int
resp chan int
}
type writeOp struct {
key int
val int
resp chan bool
}
|
func main() {
|
|
Как и прежде, мы посчитаем, сколько операций мы выполняем. |
var readOps uint64
var writeOps uint64
|
Каналы |
reads := make(chan readOp)
writes := make(chan writeOp)
|
Эта горутина, которой принадлежит состояние, она же
является картой, как в предыдущем примере, но теперь
является частной для горутины с сохранением состояния.
Она постоянно выбирает каналы |
go func() {
var state = make(map[int]int)
for {
select {
case read := <-reads:
read.resp <- state[read.key]
case write := <-writes:
state[write.key] = write.val
write.resp <- true
}
}
}()
|
Запускаем 100 горутин для выдачи операций чтения
в горутину владеющую состоянием, через канал |
for r := 0; r < 100; r++ {
go func() {
for {
read := readOp{
key: rand.Intn(5),
resp: make(chan int)}
reads <- read
<-read.resp
atomic.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
|
Так же делаем 10 записей. |
for w := 0; w < 10; w++ {
go func() {
for {
write := writeOp{
key: rand.Intn(5),
val: rand.Intn(100),
resp: make(chan bool)}
writes <- write
<-write.resp
atomic.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
|
Дадим горутинам отработать 1 секунду |
time.Sleep(time.Second)
|
Наконец, выводим данные счетчиков |
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps:", writeOpsFinal)
}
|
Запуск нашей программы показывает, что управление состоянием на основе горутин завершает около 80 000 операций. |
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177
|
Для этого конкретного случая подход, основанный на горутине, был немного более сложным, чем подход, основанный на мьютексе. Это может быть полезно в некоторых случаях, например, когда задействованы другие каналы или при управлении несколькими такими мьютексами могут возникать ошибки. Вы должны использовать тот подход, который кажется вам наиболее естественным, особенно в отношении понимания правильности вашей программы. |
Следующий пример: Сортировка (Sorting).