【问题标题】:Go channel infinite loop进入通道无限循环
【发布时间】:2017-08-21 04:06:45
【问题描述】:

我正在尝试使用通道从一组 goroutine 中捕获错误,但通道进入了无限循环,开始消耗 CPU。

func UnzipFile(f *bytes.Buffer, location string) error {
    zipReader, err := zip.NewReader(bytes.NewReader(f.Bytes()), int64(f.Len()))

    if err != nil {
        return err
    }

    if err := os.MkdirAll(location, os.ModePerm); err != nil {
        return err
    }

    errorChannel := make(chan error)
    errorList := []error{}

    go errorChannelWatch(errorChannel, errorList)

    fileWaitGroup := &sync.WaitGroup{}

    for _, file := range zipReader.File {
        fileWaitGroup.Add(1)
        go writeZipFileToLocal(file, location, errorChannel, fileWaitGroup)
    }

    fileWaitGroup.Wait()

    close(errorChannel)

    log.Println(errorList)

    return nil
}

func errorChannelWatch(ch chan error, list []error) {
    for {
        select {
        case err := <- ch:

            list = append(list, err)
        }
    }
}

func writeZipFileToLocal(file *zip.File, location string, ch chan error, wg *sync.WaitGroup) {
    defer wg.Done()

    zipFilehandle, err := file.Open()

    if err != nil {
        ch <- err
        return
    }

    defer zipFilehandle.Close()

    if file.FileInfo().IsDir() {
        if err := os.MkdirAll(filepath.Join(location, file.Name), os.ModePerm); err != nil {
            ch <- err
        }
        return
    }

    localFileHandle, err := os.OpenFile(filepath.Join(location, file.Name), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())

    if err != nil {
        ch <- err
        return
    }

    defer localFileHandle.Close()

    if _, err := io.Copy(localFileHandle, zipFilehandle); err != nil {
        ch <- err
        return
    }

    ch <- fmt.Errorf("Test error")
}

所以我正在循环一个文件片段并将它们写入我的磁盘,当出现错误时,我会向errorChannel 报告以将该错误保存到一个片段中。

我使用sync.WaitGroup 等待所有goroutines,当它们完成后我想打印errorList 并检查执行过程中是否有任何错误。

列表始终为空,即使我在writeZipFileToLocal 末尾添加ch &lt;- fmt.Errorf("test") 并且频道始终挂断。

我不确定我在这里缺少什么。

【问题讨论】:

    标签: go channel


    【解决方案1】:

    1.对于第一点,无限循环:

    引用golang language spec:

    关闭通道上的接收操作总是可以继续进行 立即,在任何之后产生元素类型的零值 已收到之前发送的值。

    所以在这个函数中

    func errorChannelWatch(ch chan error, list []error) {
        for {
            select {
            case err := <- ch:
    
                list = append(list, err)
            }
        }
    }
    

    在 ch 关闭后,这会变成一个无限循环,将 nil 值添加到 list

    试试这个:

    func errorChannelWatch(ch chan error, list []error) {
        for err := range ch {
                list = append(list, err)
        }
    }
    

    2。对于第二点,为什么您在错误列表中看不到任何内容:

    问题是这个调用:

    errorChannel := make(chan error)
    errorList := []error{}
    
    go errorChannelWatch(errorChannel, errorList)
    

    在这里,您将errorChannelWatch 传递给errorList 作为值。所以切片errorList不会被函数改变。改变的是底层数组,只要append调用不需要分配一个新的。

    为了纠正这种情况,要么将切片指针传递给errorChannelWatch,要么将其重写为对闭包的调用,捕获 errorList.

    对于第一个建议的解决方案,将errorChannelWatch 更改为

    func errorChannelWatch(ch chan error, list *[]error) {
        for err := range ch {
                *list = append(*list, err)
        }
    }    
    

    和调用

    errorChannel := make(chan error)
    errorList := []error{}
    
    go errorChannelWatch(errorChannel, &errorList)
    

    对于第二种建议的解决方案,只需将调用更改为

       errorChannel := make(chan error)
       errorList := []error{}
    
       go func() {
          for err := range errorChannel {
              errorList = append(errorList, err)
          }
       } () 
    

    3.一个小评论:

    有人可能会认为,这里存在同步问题:

    fileWaitGroup.Wait()
    
    close(errorChannel)
    
    log.Println(errorList)
    

    您如何确定在调用 close 之后没有修改 errorList?一个可能的原因是,你不知道 goroutine errorChannelWatch 还需要处理多少个值。

    您的同步对我来说似乎是正确的,正如您所做的 wg.Done() 在发送到错误通道之后,所有错误值都会 当fileWaitGroup.Wait() 返回时发送。

    但这可能会改变,如果后来有人在错误中添加缓冲 频道或更改代码。

    所以我建议至少在评论中解释同步。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多