Интервьюер: Как добиться синхронизации сопрограмм?

задняя часть

«Эпизод 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

Это то, чем я хочу поделиться с вами сегодня, добро пожаловать, чтобы оставить сообщение~

Сфокусируйся наНет публики"Дядя говорит код", берите больше галантереи, увидимся в следующем выпуске~