为什么我的代码会导致停顿或竞争条件?

Why is my code causing a stall or race condition?

出于某种原因,一旦我开始通过我的 goroutine 中的通道添加字符串,代码就会在我 运行 时停止。我认为这是一个 scope/closure 问题,所以我将所有代码直接移动到函数中,但无济于事。我查看了 Golang 的文档,所有示例看起来都与我的相似,所以我对出了什么问题一无所知。

func getPage(url string, c chan<- string, swg sizedwaitgroup.SizedWaitGroup) {
    defer swg.Done()
    doc, err := goquery.NewDocument(url)

    if err != nil{
        fmt.Println(err)
    }

    nodes := doc.Find(".v-card .info")
    for i := range nodes.Nodes {
        el := nodes.Eq(i)
        var name string
        if el.Find("h3.n span").Size() != 0{
            name = el.Find("h3.n span").Text()
        }else if el.Find("h3.n").Size() != 0{
            name = el.Find("h3.n").Text()
        }

        address := el.Find(".adr").Text()
        phoneNumber := el.Find(".phone.primary").Text()
        website, _ := el.Find(".track-visit-website").Attr("href")
        //c <- map[string] string{"name":name,"address":address,"Phone Number": phoneNumber,"website": website,};
        c <- fmt.Sprint("%s%s%s%s",name,address,phoneNumber,website)
        fmt.Println([]string{name,address,phoneNumber,website,})

    }
}

func getNumPages(url string) int{
    doc, err := goquery.NewDocument(url)
    if err != nil{
        fmt.Println(err);
    }
    pagination := strings.Split(doc.Find(".pagination p").Contents().Eq(1).Text()," ")
    numItems, _ := strconv.Atoi(pagination[len(pagination)-1])
    return int(math.Ceil(float64(numItems)/30))
}


func main() {
    arrChan := make(chan string)
    swg := sizedwaitgroup.New(8)
    zips := []string{"78705","78710","78715"}

    for _, item := range zips{
        swg.Add()
        go getPage(fmt.Sprintf(base_url,item,1),arrChan,swg)
    }
    swg.Wait()

}

编辑: 所以我通过将 sizedwaitgroup 作为参考传递来修复它但是当我删除缓冲区时它不起作用是否意味着我需要提前知道有多少元素将被发送到通道?

您的频道没有缓冲区,因此写入将阻塞,直到可以读取值为止,至少在您发布的代码中,没有读者。

问题

根据您发布的代码,根据 Colin Stewart 的回答,据我所知,您的问题实际上是阅读您的 arrChan。您写入其中,但在您的代码中没有任何地方可以读取它。

来自 the documentation :

If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.

通过缓冲通道,您的代码不再阻塞通道写入操作,该行如下所示:

c <- fmt.Sprint("%s%s%s%s",name,address,phoneNumber,website)

我的猜测是,如果您在通道大小为 5000 时仍然挂起,那是因为您在 node.Nodes 的所有循环中返回了超过 5000 个值。一旦您的缓冲通道已满,操作将阻塞,直到通道有 space,就像您正在写入无缓冲通道一样。

修复

这是一个简单的示例,向您展示了如何解决此类问题(基本上只需添加一个 reader)

package main

import "sync"

func getPage(item string, c chan<- string) {
    c <- item
}

func readChannel(c <-chan string) {
    for {
        <-c
    }
}

func main() {
    arrChan := make(chan string)
    wg := sync.WaitGroup{}
    zips := []string{"78705", "78710", "78715"}

    for _, item := range zips {
        wg.Add(1)
        go func() {
            defer wg.Done()
            getPage(item, arrChan)
        }()
    }
    go readChannel(arrChan) // comment this out and you'll deadlock
    wg.Wait()
}

您无需知道尺寸即可使用。但是你可能为了干净地退出。有时观察起来可能有点棘手,因为一旦您的主函数退出,您的程序就会退出,并且所有 goroutines 仍然 运行 立即被杀死。

作为热身示例,更改 photoionized 对此的响应中的 readChannel:

func readChannel(c <-chan string) {
  for {
      url := <-c
      fmt.Println (url)
  }
}

它只是在原始代码上增加了打印。但现在您会更清楚地了解实际发生的情况。请注意,当代码实际写入 3 时,它通常只打印两个字符串。这是因为代码会在所有写入 goroutine 完成后退出,但读取 goroutine 会因此中途中止。您可以通过在 readChannel 之前删除 "go" 来 "fix" 它(这与在 main 函数中读取频道相同)。然后你会看到打印了 3 个字符串,但程序因死锁而崩溃,因为 readChannel 仍在从通道读取,但没有人再写入它。您也可以通过在 readChannel() 中准确读取 3 个字符串来解决此问题,但这需要知道您希望接收多少个字符串。

这是我的最小工作示例(我将用它来说明其余部分):

package main

import (
    "fmt"
    "sync"
) 

func getPage(url string, c chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    c <- fmt.Sprintf("Got page for %s\n",url)
}


func readChannel(c chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    var url string
    ok := true
    for ok {
        url, ok = <- c
        if ok {
            fmt.Printf("Received: %s\n", url)
        } else {
            fmt.Println("Exiting readChannel")
        }
    }
}

func main() {
    arrChan := make(chan string)
    var swg sync.WaitGroup
    base_url := "http://test/%s/%d"
    zips := []string{"78705","78710","78715"}

    for _, item := range zips{
        swg.Add(1)
        go getPage(fmt.Sprintf(base_url,item,1),arrChan,&swg)
    }

    var wg2 sync.WaitGroup
    wg2.Add(1)
    go readChannel(arrChan, &wg2)

    swg.Wait()

    // All written, signal end to readChannel by closing the channel 
    close(arrChan)
    wg2.Wait()
}

这里我关闭通道是为了向 readChannel 发出信号,表示没有任何内容可读,因此它可以在适当的时候干净地退出。但有时您可能想要告诉 readChannel 准确读取 3 个字符串并完成。或者你可能想为每个作者开始一个 reader,每个 reader 将恰好读取一个字符串......好吧,剥猫皮的方法有很多,选择权在你。

请注意,如果您删除 wg2.Wait() 行,您的代码将等同于 photoionized 的响应,并且在写入 3 时只会打印两个字符串。这是因为代码会在所有编写器完成后退出(由 [=40 确保) =]()), 但它不会等待 readChannel 完成。

如果您改为删除 close(arrChan) 行,您的代码将在打印 3 行后因死锁而崩溃,因为代码等待 readChannel 完成,但 readChannel 等待从没有人再写入的通道读取。

如果您只是在调用 readChannel 之前删除 "go",它就相当于从 main 函数中的通道读取。它会在打印 3 个字符串后再次因死锁而崩溃,因为 readChannel 仍在读取,而当所有写入者都已经完成(并且 readChannel 已经读取了他们写入的所有内容)。这里的一个棘手点是 swg.Wait() 行将永远不会被此代码到达,因为 readChannel 永远不会退出。

如果在 swg.Wait() 之后移动 readChannel 调用 ,那么代码甚至在打印单个字符串之前就会崩溃。但这是一个不同的死锁。这次代码到达 swg.Wait() 并停在那里等待作者。第一个 writer 成功了,但是 channel 没有缓冲,所以下一个 writer 阻塞,直到有人从 channel 中读取已经写入的数据。问题是——还没有人从通道读取数据,因为 readChannel 还没有被调用。因此,它会因死锁而停止并崩溃。这个特殊的问题可以是 "fixed",但是像 make(chan string, 3) 那样使通道缓冲,因为这将允许作者继续写作,即使没有人正在从该通道读取。有时这就是你想要的。但是在这里你必须再次知道通道缓冲区中的最大消息数。在大多数情况下,它只是推迟了一个问题——只需再添加一个编写器,你就可以开始了——代码停顿并崩溃,因为通道缓冲区已满,而另一个编写器正在等待某人从缓冲区中读取。

好吧,这应该涵盖所有基础。所以,检查你的代码,看看哪种情况是你的。