书 <The Go Programming Language> 中的死锁,它是如何发生的以及为什么会发生?

Deadlock in book <The Go Programming Language>, how it would happen and why it happen?

本书中多次提到deadlock关于goroutinechannel的错误使用,我没能理解为什么会发生死锁。

我想说的第一件事是,我知道通道发送和接收将阻塞,直到有要读取的项目或要发送项目的房间,也许这就是某些 deadlock 的深层原因。但请根据书中的以下摘录给我一些解释:

第 240 页

这段代码是并发抓取url,BFS风格:

func main() {
    worklist := make(chan []string)

    // Start with the command-line arguments.
    go func() { worklist <- os.Args[1:] }()

    // Crawl the web concurrently.
    seen := make(map[string]bool)
    for list := range worklist {
        for _, link := range list {
            if !seen[link] {
                seen[link] = true
                go func(link string) {
                    worklist <- crawl(link)
                }(link)
            }
        }
    }
}

引用书的第二段:

...the initial send of the command-line arguments to the worklist must run in its own goroutine to avoid deadlock, a stuck situation in which both the main goroutine and a crawler goroutine attempt to send to each other while neither is receiving...

假设初始发送到 worklist 不在它自己的 goroutin 中,我想它是这样工作的:

  1. 主 goroutine 将初始发送到 worklist,阻塞直到收到
  2. for range 收到初始项目,因此解锁 worklist 频道
  3. 爬虫 goroutine 将其项目发送到 worklist,循环...

所以据我了解,它不会阻塞和死锁。我哪里错了?

更新:@mkopriva 帮助我意识到由于第 1 步被阻止,第 2,3 步无法访问。所以我很清楚这一点。

第 243 页

这种死锁情况可能与第 240 页相同:

func main() {
    worklist := make(chan []string) // list of URLs, may have duplicates
    unseenLinks := make(chan string) // de-duplicated URLs

    // Add command-lin arguments to worklist.
    go func() { worklist <- os.Args[1:] }()

    // Create 20 crawler goroutines to fetch each unseen link.
    for i := 0; i < 20; i++ {
        go func() {
            for link := range unseenLinks {
                foundLinks := crawl(link)
                go func() { worklist <- foundLinks }()
            }
        }()
    }

    // The main goroutine de-duplicates worklist items
    // and sends the unseen ones to the crawlers.
    seen := make(map[string]bool)
    for list := range worklist {
        for _, link := range list {
            if !seen[link] {
                seen[link] = true
                unseenLinks <- link
            }
        }
    }
}

所以如果我在 for-range 循环中省略 go,死锁是如何发生的?

在第一个片段中,初始通道发送需要在 goroutine 中完成,因为如果没有 goroutine,语句将无限期地阻塞并且执行永远不会到达从该通道接收的循环。也就是说,要从 1.2.1. 需要在 goroutine 中完成。但是,如果 1. 阻塞,则永远不会达到 2.

评论开始的地方就是执行停止的地方:

func main() {
    worklist := make(chan []string)

    worklist <- os.Args[1:]
// 
//     seen := make(map[string]bool)
//     for list := range worklist {
//         for _, link := range list {
//             if !seen[link] {
//                 seen[link] = true
//                 go func(link string) {
//                     worklist <- crawl(link)
//                 }(link)
//             }
//         }
//     }
// }

在第二个片段中,您有一个 for-range 循环遍历一个通道,这样的循环将不会退出,直到 ranged-over 通道关闭。这意味着,虽然这种循环的主体可能会继续执行,但永远不会到达 loop-with-unclosed-channel 之后的代码。

https://golang.org/ref/spec#For_range

  1. For channels, the iteration values produced are the successive values sent on the channel until the channel is closed. If the channel is nil, the range expression blocks forever.

评论开始的地方就是执行停止的地方:

func main() {
    worklist := make(chan []string)
    unseenLinks := make(chan string)

    go func() { worklist <- os.Args[1:] }()

    for i := 0; i < 20; i++ {
        for link := range unseenLinks {
//            foundLinks := crawl(link)
//            go func() { worklist <- foundLinks }()
//        }
//     }
// 
//     seen := make(map[string]bool)
//     for list := range worklist {
//         for _, link := range list {
//             if !seen[link] {
//                 seen[link] = true
//                 unseenLinks <- link
//             }
//         }
//     }
// }

在第二个片段中,我认为作者在谈论下面的 go 例程:

go func() { worklist <- foundLinks }()

Links found by crawl are sent to the worklist from a dedicated goroutine to avoid deadlock.

为什么一定要存在这个?