一个频道关闭但所有goroutine都睡着了 - 僵局

时间:2016-10-14 08:16:29

标签: go concurrency parallel-processing goroutine

是的,它看起来像是StackOverflow上最重复的问题之一,但请花几分钟时间来解决这个问题。

func _Crawl(url string, fetcher Fetcher, ch chan []string) {
    if store.Read(url) == true {
        return
    } else {
        store.Write(url)
    }

    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Printf("not found: %s\n", url)
    }
    fmt.Printf("found: %s %q\n", url, body)
    ch <- urls
}

func Crawl(url string, fetcher Fetcher) {
    UrlChannel := make(chan []string, 4)
    go _Crawl(url, fetcher, UrlChannel)
    for urls, ok := <- UrlChannel; ok; urls, ok = <- UrlChannel{
        for _, i := range urls {
            go _Crawl(i, fetcher, UrlChannel)
        }
    }
    close(UrlChannel) //The channel is closed.
 }

func main() {
   Crawl("http://golang.org/", fetcher)
}

我在循环结束后关闭频道。程序返回正确的结果,但最后会引发错误:

  

致命错误:所有goroutine都睡着了 - 死锁!

     

goroutine 1 [陈接收]:

     

main.Crawl(0x113a2f,0x12,0x1800d0,0x10432220)

     

/tmp/sandbox854979773/main.go:55 + 0x220

     

main.main()

     

/tmp/sandbox854979773/main.go:61 + 0x60

我的goroutines出了什么问题?

2 个答案:

答案 0 :(得分:2)

在第一次看之后,您可以使用以下范围进行较短的

for urls := range UrlChannel { ... }

它会迭代直到频道关闭,看起来好多了。

你的函数_Crawl()的第一个 if 也有一个早期返回,所以如果第一个条件为true,那么函数将结束并且不会传递任何内容通道,因此从该通道接收的代码将永远等待。

其他事情,在你的第二个 里面为你的每个网址创建goroutine,但你并没有等待它们,实际上那些goroutines会尝试发送一些东西到关闭渠道。似乎没有发生这种情况,因为在这种情况下,代码会出现恐慌,您可以使用WaitGroup进行此操作。

简而言之,您的代码有几种可能的死锁情况。

<强> |||超级编辑||| :

我应该写信告诉你,你的代码有点混乱,解决方案可能很简单WaitGroup,但我害怕让你感觉不好因为我找到了太多问题,但如果你我真的想学习如何编写你应该首先在代码或伪代码中思考的并发代码,而不需要并发,然后尝试添加魔法。

在你的情况下,我看到的是递归解决方案,因为url是以树形式从HTML文档中获取的,就像DFS

func crawl(url string, fetcher Fetcher) {
    // if we've visited this url just stop the recursion
    if store.Read(url) == true {
       return
    }
    store.Write(url)

    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Printf("not found: %s\n", url)
        return // early return if there's no urls there's no reason to continue
    }
    fmt.Printf("found: %s %q\n", url, body)

    // this part will change !!
    // ...
    for _, url := range urls {
        crawl(url, fetcher)
    }
    //
}

func main() {
    crawl("http://golang.org", fetcher)
}

现在第二步识别并发代码,在这种情况下很容易,因为每个url可以同时获取(有时是并行),我们要添加的只是一个WaitGroup并为每个url创建一个goroutine,现在只需要更新 来获取网址(它只是 块):

// this code will be in the comment: "this part will change !!"
//
// this var is just a thread-safe counter
var wg sync.WaitGroup

// set the WaitGroup counter with the len of urls slice
wg.Add(len(urls))

for _, url := range urls {

    // it's very important pass the url as a parameter 
    // because the var url changes for each loop (url := range)
    go func(u string) {

        // Decrement the counter (-1) when the goroutine completes
        defer wg.Done()

        crawl(u, fetcher)

    }(url)
}

wg.Wait() // wait for all your goroutines
// ...

未来的考虑,也许你想控制你必须使用像Fan In或Fan Out之类的goroutine(或worker)的数量,你可以在这里找到更多: https://blog.golang.org/advanced-go-concurrency-patternshttps://blog.golang.org/pipelines

但是,不要害怕在Go中创造成千上万的goroutines very cheap

注意:我还没有编译代码,可能在某处有一个小错误:)

答案 1 :(得分:1)

上述两种解决方案和range循环通道都存在同样的问题。 问题是在关闭通道后循环将结束,但在循环结束后关闭通道。 因此我们需要知道何时关闭打开的通道。我相信我们需要计算已开始的工作(goroutines)。但在这种情况下我只是丢失了一个计数器变量。由于这是一次巡回演习,所以不应该复杂。

func _Crawl(url string, fetcher Fetcher, ch chan []string) {
    if store.Read(url) == false {
        store.Write(url)    
        body, urls, err := fetcher.Fetch(url)
        if err != nil {
            fmt.Printf("not found: %s\n", url)
        } else {
            fmt.Printf("found: %s %q\n", url, body)
        }
        ch <- urls
    }
}

func Crawl(url string, depth int, fetcher Fetcher) {
    UrlChannel := make(chan []string, 4)
    go _Crawl(url, fetcher, UrlChannel)
    for urls := range UrlChannel {
        for _, url := range urls {
            go _Crawl(url, fetcher, UrlChannel)
        }
        depth--
        if depth < 0 {
            close(UrlChannel)
        }
    }
}