【问题标题】:Go Tour #10: Lost in concurrencyGo Tour #10:迷失在并发中
【发布时间】:2018-06-11 02:21:07
【问题描述】:

我一直在尝试完成所有围棋教程之旅,但我被困在the web crawler exercise

我以为我写完了,但是输出不一致,我没有足够的并发经验来找出原因。

Here's我的代码:

package main

import (
    "fmt"
    "sync"
)

type Fetcher interface {
    // Fetch returns the body of URL and
    // a slice of URLs found on that page.
    Fetch(url string) (body string, urls []string, err error)
}
var cache = struct {
    fetched map[string]bool
    sync.Mutex
}{fetched: make(map[string]bool)}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher, c chan []string, quit chan int) {
    if depth <= 0 {
        return
    }
    go safeVisit(url, c, quit, fetcher)
    for {
        select {
        case <- quit:
            return
        case u:= <-c:
            for _, v:= range u {
                go Crawl(v, depth -1, fetcher, c, quit)
            }
        }
    }
}
func main() {
    c := make(chan []string)
    quit := make(chan int)
    Crawl("http://golang.org/", 4, fetcher, c, quit)
}

func safeVisit(url string, c chan []string, quit chan int, fetcher Fetcher) {
    cache.Lock()
    defer cache.Unlock()
    if _, ok := cache.fetched[url] ; ok {
        quit <- 0
        return
    }
    body, urls, err := fetcher.Fetch(url)
    cache.fetched[url] = true
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Visited : %s, %q \n", url, body)
    c <- urls

}

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := f[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
    "http://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "http://golang.org/pkg/",
            "http://golang.org/cmd/",
        },
    },
    "http://golang.org/pkg/": &fakeResult{
        "Packages",
        []string{
            "http://golang.org/",
            "http://golang.org/cmd/",
            "http://golang.org/pkg/fmt/",
            "http://golang.org/pkg/os/",
        },
    },
    "http://golang.org/pkg/fmt/": &fakeResult{
        "Package fmt",
        []string{
            "http://golang.org/",
            "http://golang.org/pkg/",
        },
    },
    "http://golang.org/pkg/os/": &fakeResult{
        "Package os",
        []string{
            "http://golang.org/",
            "http://golang.org/pkg/",
        },
    },
}

这是一些示例输出

Visited : http://golang.org/, "The Go Programming Language" 
not found: http://golang.org/cmd/
Visited : http://golang.org/pkg/, "Packages" 
Visited : http://golang.org/pkg/os/, "Package os" 
**Visited : http://golang.org/pkg/fmt/, "Package fmt"** 

Process finished with exit code 0

与第一个不同,最后一个包丢失(故意在上面的星号中)

Visited : http://golang.org/, "The Go Programming Language" 
not found: http://golang.org/cmd/
Visited : http://golang.org/pkg/, "Packages" 
Visited : http://golang.org/pkg/os/, "Package os"

最后,在某些运行中甚至出现死锁:

Visited : http://golang.org/, "The Go Programming Language" 
not found: http://golang.org/cmd/
Visited : http://golang.org/pkg/, "Packages" 
Visited : http://golang.org/pkg/os/, "Package os" 
Visited : http://golang.org/pkg/fmt/, "Package fmt" 
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.Crawl(0x4bfdf9, 0x12, 0x4, 0x524220, 0xc420088120, 0xc420092000, 0xc420092060)
    /home/kostas/development/challenges/go/helloWorld.go:26 +0x201
main.main()
    /home/kostas/development/challenges/go/helloWorld.go:39 +0xab

goroutine 23 [select]:
main.Crawl(0x4bfdf9, 0x12, 0x3, 0x524220, 0xc420088120, 0xc420092000, 0xc420092060)
    /home/kostas/development/challenges/go/helloWorld.go:26 +0x201
created by main.Crawl
    /home/kostas/development/challenges/go/helloWorld.go:31 +0x123

goroutine 24 [select]:
main.Crawl(0x4c09f9, 0x16, 0x3, 0x524220, 0xc420088120, 0xc420092000, 0xc420092060)
    /home/kostas/development/challenges/go/helloWorld.go:26 +0x201
created by main.Crawl
    /home/kostas/development/challenges/go/helloWorld.go:31 +0x123

goroutine 5 [select]:
main.Crawl(0x4bfdf9, 0x12, 0x3, 0x524220, 0xc420088120, 0xc420092000, 0xc420092060)
    /home/kostas/development/challenges/go/helloWorld.go:26 +0x201
created by main.Crawl
    /home/kostas/development/challenges/go/helloWorld.go:31 +0x123

goroutine 6 [select]:
main.Crawl(0x4c0a0f, 0x16, 0x3, 0x524220, 0xc420088120, 0xc420092000, 0xc420092060)
    /home/kostas/development/challenges/go/helloWorld.go:26 +0x201
created by main.Crawl
    /home/kostas/development/challenges/go/helloWorld.go:31 +0x123

我认为它与并发和递归有关。我在 github 中看到了使用 WaitGroups 等的其他解决方案,但到目前为止还没有在 go 之旅中使用它,所以我宁愿不使用它。

更新

我弄清楚了发生了什么并正在解决这个问题。基本上有时 select 语句会陷入无限循环,因为通道退出并且 c 并不总是按预期的顺序执行。我添加了一个打印(“无事可做”)的默认案例,程序有时会永远循环,有时会以正确的方式靠运气执行。我的退出条件不对

【问题讨论】:

  • 死锁的频率如何?十分之一?我正在尝试重现它。
  • 这很不一致。有时是三分之一,有时是十分之一,然后是连续两个
  • 此特定问题的最常见原因是频道未关闭。例如这里:stackoverflow.com/questions/12398359/… - 我正在尝试测量频率,也许会提出解决方案。不是围棋专家,所以不能保证。
  • 你能显示link 用于你所遵循的教程吗?
  • tour.golang.org/concurrency/10我有更新,很快就会发布

标签: go web-crawler


【解决方案1】:

我认为情况很清楚。你的频道很乱。多个 goroutine 从同一个通道接收,golang 只是随机选择一个。

当你通过quit 发送一个零时,你永远不知道哪个 goroutine 退出了:它是由 go 调度器随机选择的。在从 c 接收之前,可能会从 quit 接收到新生成的 Crawl(即使两个通道都已准备好)。

因此,depth 是一团糟,它使safeVisit 的数量被称为不稳定,导致quit 发出不同的(随机)信号。有时仅仅退出所有生成的 goroutine 是不够的,这是一个死锁。

编辑:

首先你应该了解你的任务是什么。 Crawl 函数接受一个 url、一个 dep 和一个 fetcher,它:

  1. 获取网址
  2. 打印获取的正文
  3. 使用dep-1从获取的url生成新的Crawl队列

虽然tour要求你并行“fetch”url,但是很明显step 2和step 3必须在step 1之后发生,这意味着单个Crawl等待fetch是正常的。这意味着,不需要新的 goroutine 来调用 Fetch

在第 3 步中,每个新的 Crawl 调用都无需等待前一个调用完成,因此这些调用应该是并行的。

通过这些分析,可以得出以下代码:

func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        go Crawl(u, depth-1, fetcher)
    }
    return
}

还有一个问题:处理访问过的 url。你做得很好,不用发quit,直接改成func(string) bool,直接调用:if Visited(Url) { return }就搞定了。

附注:这次旅行真的不擅长教授并发性。您可能希望查看 go 博客文章,例如 golang 并发模式或通过通信共享内存。

【讨论】:

  • 那是正确的,你将如何解决这个问题?我对并发元素还很陌生,所以我正在努力改变我的思维方式
  • 我试着写一些关于它的想法并将其编辑成答案。我希望它有所帮助。
  • 感谢您的回答和提示,明天会看看并在我弄清楚并花一些时间后接受答案。新年快乐
猜你喜欢
  • 1970-01-01
  • 2012-08-21
  • 1970-01-01
  • 2014-12-09
  • 2012-08-26
  • 2019-05-09
  • 2011-07-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多