Go в примерах: Управление состоянием горутин (Stateful Goroutines)

В предыдущем примере мы использовали явную блокировку с мьютексами для синхронизации доступа к общему состоянию между несколькими горутинами. Другой вариант - использовать встроенные функции синхронизации для горутин и каналов для достижения того же результата. Этот подход, основанный на каналах, согласуется с идеями Go о совместном использовании памяти путем обмена данными и владения каждой частью данных ровно 1 горутиной.

package main
import (
    "fmt"
    "math/rand"
    "sync/atomic"
    "time"
)

В этом примере наше состояние будет принадлежать единственной горутине. Это гарантирует, что данные никогда не будут повреждены при одновременном доступе. Чтобы прочитать или записать это состояние, другие горутины будут отправлять сообщения горутин-владельцу и получать соответствующие ответы. Эти структуры readOp и writeOp инкапсулируют эти запросы и способ, которым владеет горутина-ответчик. In this example our state will be owned by a single goroutine. This will guarantee that the data is never corrupted with concurrent access. In order to read or write that state, other goroutines will send messages to the owning goroutine and receive corresponding replies. These readOp and writeOp structs encapsulate those requests and a way for the owning goroutine to respond.

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)

Эта горутина, которой принадлежит состояние, она же является картой, как в предыдущем примере, но теперь является частной для горутины с сохранением состояния. Она постоянно выбирает каналы чтения и записи, отвечая на запросы по мере их поступления. Ответ выполняется, сначала выполняя запрошенную операцию, а затем отправляя значение по каналу 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)
            }
        }()
    }

Дадим горутинам отработать 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).