Почему закрытие канала при отложенной панике оператора? - PullRequest
0 голосов
/ 12 октября 2019

В следующем примере подпрограмма go перекачивает значения в небуферизованный канал, и основная функция выполняет итерацию по нему.

package main

import (
    "fmt"
    "strconv"
)

var chanStr chan string

func main() {
    go pump()
    fmt.Println("iterating ...")
    for val := range chanStr {
        fmt.Printf("fetched val: %s from channel\n", val)
    }
}

func pump() {
    defer close(chanStr)
    chanStr = make(chan string)
    for i := 1; i <= 5; i++ {
        fmt.Printf("pumping seq %d into channel\n", i)
        chanStr <- "val" + strconv.Itoa(i)
    }
    //close(chanStr)
}

Функция паникует со следующим выводом:

iterating ...                                             
pumping seq 1 into channel                                
pumping seq 2 into channel                                
fetched val: val1 from channel                            

......

fetched val: val4 from channel                            
pumping seq 5 into channel                                
panic: close of nil channel                               

goroutine 5 [running]:                                    
main.pump()                                               
        C:/personal/gospace/go-rules/test.go:26 +0x1a6    
created by main.main                                      
        C:/personal/gospace/go-rules/test.go:11 +0x4e     

Однако, если я прокомментирую оператор defer и закрою сразу после цикла for в процедуре pump, получатель не паникует. В чем разница в обоих случаях? Похоже, отсрочка закрывает канал до получения значения, но обычное закрытие ждет.

Также, когда я строил с использованием детектора гонки, даже врегулярное закрытие сигнализирует о потенциальном состоянии гонки (я не могу каждый раз воссоздавать гонку). Означает ли это, что оба эти способа не подходят для грациозного закрытия канала?

ОБНОВЛЕНИЕ: Для всех комментирующих я знаю, в чем проблема. Я должен создать канал в первой строке функции main(). Однако я работаю на Windows с Go1.12, и я заметил это поведение. Очевидно, я не подделал вывод. Я последовательно воссоздаю панику, используя оператор defer, и даже не однажды паника возникла, когда я закрыл канал сразу после цикла for в pump()

Ответы [ 3 ]

3 голосов
/ 12 октября 2019

Ваш код очень ненормальный, по-разному:

  1. У вас есть потенциал (на самом деле, очень вероятно), что вы начнете читать с канала в вашем for val цикл, прежде чем программа фактически инициализирует канал, приводя к тупику.

    iterating ...
    pumping seq 1 into channel
    fatal error: all goroutines are asleep - deadlock!
    

    Фактически, это единственное поведение, которое я наблюдаю, выполняя ваш код, как есть, как локально, так и на игровой площадке. .

  2. Если я добавлю задержку,

     fmt.Println("iterating ...")
     time.Sleep(10 * time.Millisecond) // Delay ensures the channel has been created
     for val := range chanStr {
    

    Затем я наблюдаю ваше отмеченное поведение:

    iterating ...
    pumping seq 1 into channel
    fetched val: val1 from channel
    pumping seq 2 into channel
    pumping seq 3 into channel
    fetched val: val2 from channel
    fetched val: val3 from channel
    pumping seq 4 into channel
    pumping seq 5 into channel
    fetched val: val4 from channel
    fetched val: val5 from channel
    panic: close of nil channel
    

    Причина этого заключается в том, чточто вы звоните close(chanStr), а chanStr все еще ноль. Если вы позвоните своему defer после создания канала:

    func pump() {
        chanStr = make(chan string)
        defer close(chanStr)
    

    , вы решите эту проблему.

Чтобы решить обе расы, вам нужно инициализироватьканал до , вызывающий программу. Полный код:

package main

import (
    "fmt"
    "strconv"
)

var chanStr chan string

func main() {
    chanStr = make(chan string)
    go pump(chanStr)
    fmt.Println("iterating ...")
    for val := range chanStr {
        fmt.Printf("fetched val: %s from channel\n", val)
    }
}

func pump(chanStr chan string) {
    defer close(chanStr)
    for i := 1; i <= 5; i++ {
        fmt.Printf("pumping seq %d into channel\n", i)
        chanStr <- "val" + strconv.Itoa(i)
    }
}

Чтобы дополнительно проиллюстрировать, что проблема заключается в том, что defer close(chanStr) оценивает chanStr немедленно (пока он все еще nil), рассмотрите это (не рекомендуется!) Альтернативное решение:

package main

import (
    "fmt"
    "strconv"
    "time"
)

var chanStr chan string

func main() {
    go pump()
    fmt.Println("iterating ...")
    time.Sleep(10 * time.Millisecond)
    for val := range chanStr {
        fmt.Printf("fetched val: %s from channel\n", val)
    }
}

func pump() {
    defer func() {
        close(chanStr)
    }()
    chanStr = make(chan string)
    for i := 1; i <= 5; i++ {
        fmt.Printf("pumping seq %d into channel\n", i)
        chanStr <- "val" + strconv.Itoa(i)
    }
}

В этом случае отложенной функцией является замыкание на chanStr, поэтому оценка chanStr задерживается до фактического выполнения. В этой версии, когда выполняется отложенная функция, chanStr больше не равен нулю, поэтому паники нет.

0 голосов
/ 12 октября 2019

Существует вероятность, что основная подпрограмма может считывать данные с канала перед его созданием. Это ваша гонка данных.

Канал должен быть создан до запуска вашей обычной программы.

Исправление: https://play.golang.org/p/O7pgM05KEtI

0 голосов
/ 12 октября 2019

Код, который вы разместили, имел сценарий тупика. Как указывал Флимзи, вы, вероятно, не размещали одинаковые коды.

Вот обновленный код, который должен работать:

package main

import (
    "fmt"
    "strconv"
)

func main() {

    chanStr := make(chan string)

    go pump(chanStr)
    fmt.Println("iterating ...")
    for val := range chanStr {
        fmt.Printf("fetched val: %s from channel\n", val)
    }
}

func pump(ch chan string) {
    defer close(ch)

    for i := 1; i <= 5; i++ {
        fmt.Printf("pumping seq %d into channel\n", i)
        ch <- "val" + strconv.Itoa(i)
    }
    //close(chanStr)
}

Проблема в этом случае заключалась в том, что вы создали канал внутри функции pump, поэтому основная функция не знала, какпотреблять данные и вызывает тупик, потому что не было ни одного потребителя.

...