Stateful Goroutines
#
// Ми скористались [mutex](mutexes)'ами для синхронізації
// доступу до спільного стану в минулому прикладі. Іншим варіантом
// буде використання [горутин](goroutines) та [каналів](channels)
// задля отримання того ж ефекту. Цей спосіб також є ідіоматичним
// у Go - так ми можемо працювати з спільною пам'яттю
// віддаючи її у володіння лише 1-й горутині.
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
// У цьому прикладі наш стан буде переданий у володіння
// лише одній горутині. Це гарантуватиме, що дані ніколи не будуть
// зіпсовані одночасним доступом. Щоб читати та писати стан
// - інші оперуючі горутини будуть надсилати повідомлення
// головній горутині і отримувати належні відповіді.
// Ці структури - `readOp` та `writeOp`, приховують запити
// і те як головна горутина відповідає.
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` та `writes`, будуть використані
// іншими горутинами для створення запитів на запис або на
// читання відповідно.
writes := make(chan *writeOp)
reads := make(chan *readOp)
// Ця горутина - є власником стану (який був мапою в
// попередньому прикладі), який, наразі є приватним для
// горутини зі станом. Ця горутина у циклі отримує запити
// на читання та запис з відповідних каналів (зверніть увагу
// на `select`) і відповідає на запити як тільки ті надходять.
// Відповідь відбувається шляхом виконання операції (яку було
// запитано) та відправкою відповіді до каналу `resp` (індикація
// успіху або бажане значення у випадку `reads`).
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 горутин, що будуть надсилати запити
// зчитування до головної горутини через канал `reads`.
// Кожна горутина створює `readOp` та надсилає її (змінну) до
// каналу `reads` і отримує відповідь з каналу `resp`.
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)
}
}()
}
// Даємо нашим горутинам попрацювати близько секунди.
time.Sleep(time.Second)
// Нарешті, дивимось скільки було зчитувань та записів:
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps:", writeOpsFinal)
}
# Запуск програми продемонструє що наш
# базований на горутинах приклад управління
# станом проводить біля 80-85 тис операцій.
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177
# Конкретно цей прaклад був трошка менш
# задіяний в роботі ніж базований на [mutex](mutexes)-ах.
# Слідує використовувати той спосіб який
# вам більш до впоодби, з урахуванням
# розуміння ситуації наскільки коректно
# працює ваша програма.