《Go 编程语言》一书中的死锁,它是如何发生的以及为啥会发生?

Posted

技术标签:

【中文标题】《Go 编程语言》一书中的死锁,它是如何发生的以及为啥会发生?【英文标题】:Deadlock in book <The Go Programming Language>, how it would happen and why it happen?《Go 编程语言》一书中的死锁,它是如何发生的以及为什么会发生? 【发布时间】:2021-11-10 05:11:26 【问题描述】:

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

我要说的第一件事是,我知道频道 send&receive 会阻塞,直到有要读取的项目或要发送项目的房间,这也许是某些 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)
            
        
    

引用书的第二段:

...命令行参数到工作列表的初始发送必须在其自己的 goroutine 中运行以避免死锁,这是一种卡住的情况,主 goroutine 和爬虫 goroutine 都尝试相互发送,而两者都没有接收...

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

    main goroutine 发送初始值到worklist,阻塞直到收到 for range 接收初始项目,因此解封worklist 频道 爬虫 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,死锁是如何发生的?

【问题讨论】:

“我想它是这样工作的:....” -- 在你想象的执行模型中,如果1 阻塞你如何到达2 对...我才意识到这一点。我的心被堵住了。仍在处理第二个代码 sn-p。 如果不清楚,如果你从循环中删除 goroutine,第二个 sn-p 也会有同样的问题。 for link := range unseenLinks 循环不会退出,直到某事关闭 unseenLinks. @mkopriva 我明白了。谢谢!你能写下答案让我接受吗? 【参考方案1】:

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

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

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

在第二个 sn-p 中,您有一个通道上的 for-range 循环,这样的循环在远程通道关闭之前不会退出。这意味着,虽然这样的循环体可能会继续执行,但永远不会到达 loop-with-unclosed-channel 之后的代码。

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

    对于通道,生成的迭代值是通道上发送的连续值直到通道为closed。如果频道 为 nil,范围表达式将永远阻塞。

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

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 通道死锁?

死锁的概念以及发生死锁的缘由

死锁产生的条件以及解决方法

java并发编程04:死锁

Oracle常见死锁发生的原因以及解决方法

Go36-26-互斥锁与读写锁