为什么在延迟语句中关闭通道会出现恐慌?

时间:2019-10-12 17:02:21

标签: go goroutine

在下面的示例中,go例程将值泵入未缓冲的通道,并且主要功能对其进行迭代。

import React, { useEffect, useState } from 'react';
import { Select, Input, MenuItem } from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import { setTags } from "../actions/search";

const MultiChipSelect = ({ source }) => {

    const dispatch = useDispatch();
    const selectedTags = useSelector(state => state.search.tags);
    const sourceTags = useSelector(state => state.settings[source].tags);

    const handleTagClick = async (e) => {
        dispatch(setTags(e.target.value));
    };

    return (
        <Select
        multiple
        value={selectedTags}
        onChange={handleTagClick}
        input={<Input id="select-multiple" />}
        // renderValue={selected => ( // This function is necessary when using objects, because the 'label' value should display the value.data value, not just the object. This is necessary because I'm using objects instead of strings.
        //    <div>
        //      {selected.map(value => (
        //        <Chip key={value._id} label={value.data}/>
        //      ))}
        //    </div>
        //  )}
        >
        >
        {sourceTags.map(val => (
            <MenuItem key={val._id} value={val}>
            {val.data}
            </MenuItem>
        ))}
        </Select>
    );
}

export default MultiChipSelect;

函数出现紧急输出:

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)
}

但是,如果我注释defer语句并在goroutine 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 中的for循环之后立即关闭,则接收方不会惊慌。 这两种情况有什么区别?看起来defer在收到值之前关闭了通道,但是常规关闭等待。

此外,当我使用比赛检测器进行构建时,即使在常规关闭中,它也会标记潜在的比赛条件(我无法每次都重新创建比赛)。是否暗示这两种方式都不适合正常关闭通道?

更新: 对于所有这些评论,我知道出了什么问题。我必须在pump函数的第一行中创建通道。但是我在带有go1.12的Windows上运行,并且观察到了这种现象。 显然我没有伪造输出。我一直使用defer语句重新创建恐慌,即使在main()

中的for循环后立即关闭通道时也没有发生恐慌。

3 个答案:

答案 0 :(得分:3)

您的代码以不同的方式非常活泼:

  1. 在goroutine实际初始化通道之前,您有可能(实际上很有可能)从for val循环中的通道开始读取,从而导致死锁。 / p>

    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)仍为nil时正在调用chanStr。如果您在创建频道后致电defer

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

    您将解决该问题。

要解决这两个种族,您需要在调用goroutine之前初始化通道 。完整的代码:

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不再为nil,因此不会出现紧急情况。

答案 1 :(得分:0)

主go例程可能会在创建之前从通道读取。那就是你的数据竞赛。

应该在开始程序前 创建通道。

修正:https://play.golang.org/p/O7pgM05KEtI

答案 2 :(得分:-1)

您发布的代码出现了死锁情况。正如Flimzy所指出的,您可能没有发布相同的代码。

这是应该起作用的更新代码:

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函数内部创建了通道,因此主函数不知道如何使用数据,并且由于没有使用者,因此导致了死锁。