【问题标题】:Why is my code causing a stall or race condition?为什么我的代码会导致停顿或竞争状况?
【发布时间】:2017-03-05 07:09:21
【问题描述】:

由于某种原因,一旦我开始通过 goroutine 中的通道添加字符串,代码在运行时就会停止。我认为这是一个范围/关闭问题,所以我将所有代码直接移到函数中,但无济于事。我查看了 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)也不是最小示例。你可以提供实际的错误。
  • 比赛错误-
  • 提供赏金固然不错,但如果您想得到答案,则应首先遵守此处发布的 cmets。首先,提供Minimal, Complete, and Verifiable example。并将该示例设为您最近的最新代码,因为您声称您已对代码进行了多次更改,但我没有看到它们反映在您在问题中发布的代码中。

标签: go concurrency goroutine


【解决方案1】:

问题

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

来自the documentation

如果通道没有缓冲,发送方会阻塞,直到接收方收到该值。如果通道有缓冲区,则发送方仅阻塞直到值 已复制到缓冲区;如果缓冲区已满,这意味着 等到某个接收者检索到一个值。

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

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

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

修复

这是一个最小的示例,向您展示如何解决此类问题(基本上只需添加一个阅读器)

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()
}

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 好的,所以我在通道中添加了一个 5000 的缓冲区,并在我的 main() 方法中循环通过通道,它仍然挂起
    • 所以我通过传递 sizedwaitgroup 作为参考来修复它,但是当我删除缓冲区时它不起作用,这是否意味着我需要知道有多少元素将提前发送到通道?
    【解决方案3】:

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

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

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

    它只在原始代码中添加打印。但是现在你会更好地看到实际发生的事情。请注意,当代码实际写入 3 时,它通常只打印两个字符串。这是因为一旦所有写入 goroutine 完成,代码就会退出,但读取 goroutine 会因此中途中止。您可以通过在 readChannel 之前删除“go”来“修复”它(这与在 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 个字符串并完成。或者您可能希望为每个作者启动一个阅读器,每个阅读器将只读取一个字符串...嗯,有很多方法可以给猫剥皮,您可以选择。

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-02-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多