Понимание проблемы Golang Код параллелизма - PullRequest
1 голос
/ 13 июля 2020

Я только начал изучать golang, пока я проходил через параллелизм, я случайно написал этот код:


import ( 
    "fmt"
)

func squares(c chan int) {
    for i := 0; i < 4; i++ {
        num := <- c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println("main start")
    
    c := make(chan int)
    
    go squares(c)
    
    c <- 1
    c <- 2
    c <- 3
    c <- 4
    
    go squares(c)
    
    c <- 5
    c <- 6
    c <- 7
    c <- 8
    
    fmt.Println("main stop")
}

Первоначально я предполагал назначить c := make(chan int, 3).

Я возникли проблемы с пониманием вывода написанного мной кода.

main start
1
4
9
25
36
49
16
main stop

Я хотел бы понять, как выполняется код. Я ожидал ошибки: все горутины спят - тупик!

Большое спасибо!

1 Ответ

3 голосов
/ 13 июля 2020

Я не уверен, что действительно понимаю, чего вы хотели достичь, особенно по причине этого странного l oop:

    for i := 0; i < 4; i++ {
        num := <- c
        fmt.Println(num * num)
    }

Но в любом случае. Прежде всего, несколько объяснений того, как работают каналы и горутины.

Канал - это потокобезопасный канал обмена сообщениями, используемый для обмена данными в разных контекстах выполнения. Канал создается с помощью инструкции make, тогда

c := make(chan int, 3)

означает создание канала типа int с размером «буфера» 3. Этот элемент очень важен для понимания. Канал действительно следует шаблону производитель / потребитель с этими базовыми правилами:

Для производителя:

  • если я попытаюсь передать sh некоторые данные в канале с некоторым «свободным пространством» , он не блокируется, и выполняются следующие инструкции
  • Если я пытаюсь передать sh некоторые данные в канале без «свободного места», он блокируется до тех пор, пока не будет обработана предыдущая

Для потребителя:

  • Если я пытаюсь взять элемент из пустого канала, он блокируется до тех пор, пока не появятся некоторые данные
  • Если я пытаюсь взять элемент из непустого канала, он возьмите первый (канал - это FIFO)

Обратите внимание, что все операции блокировки могут быть отключены с помощью некоторого специального шаблона, доступного с помощью инструкции select, но это уже другая тема :).

Горутины - это легкие подпроцессы подпрограммы (без потоков). Здесь нет места для подробного объяснения всех деталей, но важно то, что 1 горутина === 1 стек выполнения.

Итак, в вашем случае результат вполне предсказуем, пока не останется только один потребитель. После того, как вы запустите вторую горутину, порядок потребления по-прежнему предсказуем (размер канала только один), а порядок выполнения - нет!

Примечательно одно: у вас всего 7 выходов ... потому что ваша основная горутина завершается до того, как это произойдет, завершая выполнение всей программы. Этот момент объясняет, почему у вас не было all goroutines are asleep - deadlock!.

Если вы хотите его иметь, вы просто должны добавить <- c где-нибудь в конце основного:

package main

import (
    "fmt"
)

func squares(c chan int) {
    for i := 0; i < 4; i++ {
        num := <-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println("main start")

    c := make(chan int)

    go squares(c)

    c <- 1
    c <- 2
    c <- 3
    c <- 4

    go squares(c)

    c <- 5
    c <- 6
    c <- 7
    c <- 8

    <-c
    fmt.Println("main stop")
}

У вас будет поведение, которое, как я думаю, вы ожидаете:

main start
1
4
9
25
36
49
64
16
fatal error: all goroutines are asleep - deadlock!

Редактировать: шаг за шагом, выполнение:

    // a goroutine is created and c is empty. because
    // the code of `squares` act as a consumer, the goroutine
    // "block" at instruction `num := <-c`, but not the main goroutine
    go squares(c)
    
    // 1 is pushed to the channel. Till "c" is not full the main goroutine
    // doesn't block but the other goroutine may be "released"
    c <- 1

    // if goroutine of squares has not consume 1 yet, main goroutine block   
    // untill so, but is released just after
    c <- 2

    // it continues with same logic
    c <- 3
    c <- 4

    // till main goroutine encountered `<- c` (instruction I added) .
    // Here, it behave as a consumer of "c". At this point all
    // goroutine are waiting as consuler on "c" => deadlock
...