Введение
Этот лаба旨在演示如何使用каналы и горутины для синхронизации доступа к общему состоянию между несколькими горутинами.
This tutorial is from open-source community. Access the source code
💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал
Этот лаба旨在演示如何使用каналы и горутины для синхронизации доступа к общему состоянию между несколькими горутинами.
В параллельном программировании至关重要的是同步对共享状态的访问,以避免竞争条件和数据损坏。此实验室展示了一种场景,其中单个 горутина拥有状态,而其他 горутины发送消息以读取或写入该状态。
readOp
和 writeOp
结构体来封装请求和响应。resp
通道来指示成功并返回值。atomic
包来统计读取和写入操作。time
包在操作之间添加延迟。## Запуск нашей программы показывает, что пример управления
## состоянием на основе горутин завершает примерно 80 000
## операций в сумме.
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177
## В этом конкретном случае подход на основе горутин был
## немного более сложным, чем подход на основе мьютексов.
## Однако он может быть полезен в некоторых случаях,
## например, когда вы используете другие каналы или когда
## управление несколькими такими мьютексами может привести
## к ошибкам. Вы должны использовать тот подход, который
## кажется наиболее естественным, особенно в отношении
## понимания правильности вашей программы.
Ниже представлен полный код:
// В предыдущем примере мы использовали явное блокирование с
// помощью [мьютексов](mutexes), чтобы синхронизировать доступ
// к общему состоянию между несколькими горутинами.
// Другой вариант — использовать встроенные функции
// синхронизации горутин и каналов, чтобы достичь того же
// результата. Этот подход на основе каналов соответствует
// идеям Go о разделении памяти путём коммуникации и
// обеспечением того, чтобы каждая часть данных была
// принадлежащей ровно одной горутине.
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
// В этом примере наше состояние будет принадлежать одной
// горутине. Это гарантирует, что данные никогда не будут
// повреждены при одновременном доступе. Чтобы прочитать или
// записать это состояние, другие горутины будут отправлять
// сообщения в горутину-владельца и получать соответствующие
// ответы. Эти структуры `readOp` и `writeOp` encapsulate
// эти запросы и способ ответа для горутины-владельца.
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` будут использоваться другими
// горутинами для отправки запросов на чтение и запись,
// соответственно.
reads := make(chan readOp)
writes := make(chan writeOp)
// Вот горутина, которая владеет `state`, которое является
// картой, как в предыдущем примере, но теперь приватной
// для состояятельной горутины. Эта горутина постоянно
// выбирает между каналами `reads` и `writes`,
// реагируя на запросы по их поступлению. Ответ выполняется
// путём предварительного выполнения запрошенной операции
// и последующей отправки значения по каналу ответа `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)
}
Этот лаба показал, как использовать каналы и горутины для синхронизации доступа к общему состоянию. Сделав одну горутину владельцем состояния и используя каналы для отправки запросов на чтение и запись, мы можем избежать ситуаций гонки и повреждения данных.