❝«Эпизод 9» До 80-го маленького гола дядюшки еще 71 выпуск, какой дядя хочет поделиться с вами сегодняНесколько способов реализации синхронизации сопрограмм в golang, да это все-таки фундамент, а прочный фундамент никогда не устареет.Кроме того, как вы думаете, эта тема более привычна в интервью? Давайте узнаем вместе.
❞
Зачем делать синхронизацию
Прежде чем перейти к делу, давайте сначалаПривычно трогать свою совесть и спрашивать себя (дзи) себя (дзи): Зачем синхронизировать?
Предполагая, что несколько сопрограмм одновременно обращаются к данным в одной и той же памяти и обрабатывают их, тогда первая сопрограмма может просто скопировать данные из регистра в память за последнюю наносекунду, а вторая сопрограмма немедленно использует их для изменения данных. Значение перезаписывается, поэтому общая переменная данных будет перепутана.
Возьмите каштан:
package main
import(
"fmt"
"time"
)
var share_cnt uint64 = 0
func incrShareCnt() {
for i:=0; i < 10000; i++ {
share_cnt++
}
}
func main() {
for i:=0; i < 2; i++ {
go incrShareCnt()
}
time.Sleep(10*time.Second)
fmt.Println(share_cnt)
}
Приведенный выше код использует 2 сопрограммы для одновременного добавления глобальной переменной по 1 000 000 раз каждая.Давайте посмотрим на результаты распечатки:
dashu@dashu > /data1/htdocs/go_practice > go run test.go
1014184
dashu@dashu > /data1/htdocs/go_practice > go run test.go
1026029
dashu@dashu > /data1/htdocs/go_practice > go run test.go
19630
...
Из результатов печати мы видим, что хотя мы увеличиваем глобальную переменную 20 000 раз в коде, результат 20 000 раз не печатается, потому что перезапись данных происходит, когда данные совместно используются сопрограммами. На самом деле код выше не выведет 20000, если он скучный и долго спит.
Метод синхронизации сопрограммы
Итак, как можно синхронизировать данные между горутинами? Вам доступны следующие три метода синхронизации данных:
- time.Sleep
- channel
- sync.WaitGroup
time.Sleep
Почему Sleep может использовать для синхронизации данных? Возьмем каштан:
func main() {
go func() {
fmt.Println("goroutine1")
}()
go func() {
fmt.Println("goroutine2")
}()
}
Выполните приведенный выше код, и вы обнаружите, что вывода нет, причина в следующем:Основная сопрограмма завершилась до того, как будут выполнены две сопрограммы, и все остальные сопрограммы будут завершены, когда завершится основная сопрограмма.Таким образом, результатом, который заставляет код работать, является ничто.
Мы немного спим, прежде чем закончится основная сопрограммаможет появитьсяРезультаты:
func main() {
go func() {
fmt.Println("goroutine1")
}()
go func() {
fmt.Println("goroutine2")
}()
time.Sleep(time.Second)
}
Распечатка:
goroutine1
goroutine2
Почему я говорю выше"может появиться"Шерстяная ткань? В приведенном выше коде мы устанавливаем время ожидания на 1 с.Поскольку логика обработки сопрограммы относительно проста, приведенные выше результаты могут быть распечатаны нормально, если я выполняю очень сложные логические операции (время больше 1 с) в этих двух сопрограммах , то Вы обнаружите, что результат все еще не напечатан.
Итак, возникает еще один вопрос:Мы не можем быть уверены, как долго нам нужно спать
Похоже, что этот SLEP действительно ненадежен.Есть ли способ заменить SLEEP? Ответ однозначно, давайте посмотрим второй способ.
канал
Как канал достигает синхронизации с горутиной? Давайте посмотрим на типичный каштан:канал реализует простой производитель и потребитель
package main
import (
"fmt"
"time"
)
func producer(ch chan int, count int) {
for i := 1; i <= count; i++ {
fmt.Println("大妈做第", i, "个面包")
ch <- i
// 睡眠一下,可以让整个生产消费看得更清晰点
time.Sleep(time.Second * time.Duration(1))
}
}
func consumer(ch chan int, count int) {
for v := range ch {
fmt.Println("大叔吃了第", v, "个面包")
count--
if count == 0 {
fmt.Println("没面包了,大叔也饱了")
close(ch)
}
}
}
func main() {
ch := make(chan int)
count := 5
go producer(ch, count)
consumer(ch, count)
}
В приведенном выше коде мы настроили еще одну горутину, позволяющую тете производить 5 хлебов (фактически записывать данные в канал), а основная горутина заставляет дядю постоянно есть хлеб (считывать данные из канала). Давайте посмотрим на вывод:
大妈做第 1 个面包
大叔吃了第 1 个面包
大妈做第 2 个面包
大叔吃了第 2 个面包
大妈做第 3 个面包
大叔吃了第 3 个面包
大妈做第 4 个面包
大叔吃了第 4 个面包
大妈做第 5 个面包
大叔吃了第 5 个面包
没面包了,大叔也饱了
Из результатов вывода мы видим, что тетя испекла всего 5 хлебов, а дядя съел всего 5 хлебов, которые синхронизированы!
"Tip":
В приведенном выше коде мы используем for-range для чтения данных канала.For-range — очень характерный оператор со следующими характеристиками:
- Если канал был закрыт, он будет продолжать выполняться до тех пор, пока не будут получены все значения, а затем прекратит выполнение.
- Если канал не закрыт, но в канале нет данных для чтения, он будет заблокирован в позиции диапазона, пока не будет разбужен.
- Если канал нулевой, то он также соответствует принципу, о котором мы сказали выше, и чтение будет заблокировано, то есть всегда будет заблокировано в позиции диапазона.
Давайте проверим, мы помещаем приведенный выше код вclose(ch)Перейдите к основной сопрограмме и попробуйте:
package main
import (
"fmt"
"time"
)
func producer(ch chan int, count int) {
for i := 1; i <= count; i++ {
fmt.Println("大妈做第", i, "个面包")
ch <- i
// 睡眠一下,可以让整个生产消费看得更清晰点
time.Sleep(time.Second * time.Duration(1))
}
}
func consumer(ch chan int, count int) {
for v := range ch {
fmt.Println("大叔吃了第", v, "个面包")
count--
if count == 0 {
fmt.Println("没面包了,大叔也饱了")
}
}
}
func main() {
ch := make(chan int)
count := 5
go producer(ch, count)
consumer(ch, count)
close(ch)
}
распечатка:
大妈做第 1 个面包
大叔吃了第 1 个面包
大妈做第 2 个面包
大叔吃了第 2 个面包
大妈做第 3 个面包
大叔吃了第 3 个面包
大妈做第 4 个面包
大叔吃了第 4 个面包
大妈做第 5 个面包
大叔吃了第 5 个面包
没面包了,大叔也饱了
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.consumer(0xc00008c060, 0x0)
/data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:19 +0x5f
main.main()
/data1/htdocs/go_project/src/github.com/cnyygj/go_practice/test.go:32 +0x7c
exit status 2
Конечно, он заблокировался, и в конце концов образовался тупик, и было выдано исключение.
sync.WaitGroup
Если вы считаете, что двух вышеуказанных методов недостаточно, давайте рассмотрим еще один метод:sync.WaitGroup
WaitGroup реализует внутренний счетчик для записи количества незавершенных операций. Он предоставляет три метода:
- Add() используется для добавления счетчика
- Done() вызывается в конце операции для уменьшения счетчика на единицу.[Я не скажу вам, что реализация метода Done() на самом деле вызывает Add(-1)]
- Wait() используется для ожидания окончания всех операций, то есть счетчик становится равным 0, функция будет ждать, когда счетчик не равен 0, и немедленно возвращаться, когда счетчик равен 0.
Или посмотрите на каштаны:
func main() {
var wg sync.WaitGroup
wg.Add(2) // 因为有两个动作,所以增加2个计数
go func() {
fmt.Println("Goroutine 1")
wg.Done() // 操作完成,减少一个计数
}()
go func() {
fmt.Println("Goroutine 2")
wg.Done() // 操作完成,减少一个计数
}()
wg.Wait() // 等待,直到计数为0
}
распечатка:
Goroutine 1
Goroutine 2
Это то, чем я хочу поделиться с вами сегодня, добро пожаловать, чтобы оставить сообщение~
Сфокусируйся наНет публики"Дядя говорит код", берите больше галантереи, увидимся в следующем выпуске~