Golang中的死锁错误

3
我最近接触了go语言并且被它迷住了,看起来非常有趣!完成教程后,我想自己构建一些东西:我想列出我的音乐库中的所有歌曲。我认为在这里可以利用go的并发性能。当例行程序遍历目录树时,它会将音乐文件(路径)推送到一个通道中,然后由另一个例行程序读取ID3标签,这样我就不必等待每个文件都被找到。
这是我的简单而天真的方法:
package main

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
    "sync"
)

const searchPath = "/Users/luma/Music/test" // 5GB of music.

func main() {
    files := make(chan string)

    var wg sync.WaitGroup
    wg.Add(2)

    go printHashes(files, &wg)
    go searchFiles(searchPath, files, &wg)

    wg.Wait()
}

func searchFiles(searchPath string, files chan<- string, wg *sync.WaitGroup) {
    visit := func(path string, f os.FileInfo, err error) error {
        if !f.IsDir() && strings.Contains(".mp4.mp3.flac", filepath.Ext(f.Name())) {
            files <- path
        }
        return err
    }

    if err := filepath.Walk(searchPath, visit); err != nil {
        fmt.Println(err)
    }

    wg.Done()
}

func printHashes(files <-chan string, wg *sync.WaitGroup) {
    for range files {
        fmt.Println(<-files)
    }

    wg.Done()
}

目前该程序尚未读取标签,它只会打印文件路径。虽然如此,它仍能快速列出所有音乐文件!但是,在程序完成后我看到了以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc42007205c)
    /usr/local/Cellar/go/1.7.4_2/libexec/src/runtime/sema.go:47 +0x30
sync.(*WaitGroup).Wait(0xc420072050)
    /usr/local/Cellar/go/1.7.4_2/libexec/src/sync/waitgroup.go:131 +0x97
main.main()
    /Users/luma/Code/Go/src/github.com/LuMa/test/main.go:22 +0xfa

goroutine 17 [chan receive]:
main.printHashes(0xc42008e000, 0xc420072050)
    /Users/luma/Code/Go/src/github.com/LuMa/test/main.go:42 +0xb4
created by main.main
    /Users/luma/Code/Go/src/github.com/LuMa/test/main.go:19 +0xab
exit status 2

什么导致了死锁?

始终延迟WaitGroup.Done()。您的第一个goroutine中存在一个返回路径,该路径未调用Done。 - JimB
1
@JimB,那差点也让我犯错了。但请注意,这不是一个返回路径——它是一个内联函数,声明并传递到另一个函数中——只有一条执行路径。但是,通常的做法是调用 defer wg.Done() - eduncan911
@eduncan911:哎呀,谢谢提醒。我只是扫描了一下早期返回的内容,没有仔细阅读。 - JimB
谢谢你的提示! - LuMa
2个回答

2

因为你需要关闭 files 通道。

在你的情况下,你没有关闭它,所以

for range files { fmt.Println(<-files) } 会等待从 files 通道获取值。所以 printHashes 中的 wg.Done() 永远不会完成。

func searchFiles(searchPath string, files chan<- string, wg *sync.WaitGroup) {
    visit := func(path string, f os.FileInfo, err error) error {
        if !f.IsDir() && strings.Contains(".mp4.mp3.flac", filepath.Ext(f.Name())) {
            files <- path
        }
        return err
    }

    if err := filepath.Walk(searchPath, visit); err != nil {
        fmt.Println(err)
    }

    wg.Done()
    close(files) // close the chanel, because you don't put thing into the channel anymore.
}

1

searchFiles 中,当发送完成后,您希望执行 close(files)。这个约定被称为发送者关闭(接收者从不关闭)。同时,删除对 wg.Done() 的调用,因为您尚未完成...通道中仍可能有项目。

close(files) 将发出信号,使 for range files 关闭并退出循环,这将调用您的 wg.Done() 来向主函数发出所有操作已完成的信号。

(未在移动设备上测试)

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "strings"
    "sync"
)

const searchPath = "/Users/luma/Music/test" // 5GB of music.

func main() {
    files := make(chan string)

    var wg sync.WaitGroup
    wg.Add(1)

    go printHashes(files)
    go searchFiles(searchPath, files, &wg)

    wg.Wait()
}

func searchFiles(searchPath string, files chan<- string) {
    visit := func(path string, f os.FileInfo, err error) error {
        if !f.IsDir() && strings.Contains(".mp4.mp3.flac", filepath.Ext(f.Name())) {
            files <- path
        }
        return err
    }

    if err := filepath.Walk(searchPath, visit); err != nil {
        fmt.Println(err)
    }
    close(files)
}

func printHashes(files <-chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    for range files {
        fmt.Println(<-files)
    }
}

请注意,虽然这看起来很快,但使用单个goroutine也可以正常工作,并且不会阻塞主goroutine。但是,如果您尝试在多个goroutine中读取多个文件以获取id3标签,则可能不会获得任何优势-它们都将在系统调用级别上共享相同的文件I / O锁定。唯一有利的方式是,如果数据处理远远超过文件I / O锁定(例如,计算中存在大量内容,因为处理远比系统调用锁定快)。欢迎加入Go社区!

谢谢您提供详细的答案,并提示有关syscall级别文件锁的信息。我的最初想法是,遍历一个500GB音乐库的目录树可能需要一些时间。那么为什么不尽早开始读取标签(这要慢得多呢)呢? - LuMa
1
@LuMa 实际上,读取MP3标签非常快。已经知道了MP3中字节码偏移量的标签位置。因此,这是一个快速的“打开文件->读取字节偏移位置->关闭文件”操作。系统调用仍然是主要瓶颈,这将影响性能。除非它们在不同的驱动器上。 :) - eduncan911

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接