繁体   English   中英

为什么我的代码导致失速或竞赛状况?

[英]Why is my code causing a stall or race condition?

由于某种原因,一旦我开始通过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()

}

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

问题

据我所知,根据您发布的代码,以科林·斯图尔特的答案为基础,实际上,您遇到的问题是阅读arrChan 您可以编写代码,但是在代码中没有地方可以读取它。

文档中

如果通道未缓冲,则发送方将阻塞,直到接收方收到该值为止。 如果通道具有缓冲区,则发送方仅在将值复制到缓冲区之前才阻塞; 如果缓冲区已满,则意味着要等到某些接收者检索到一个值。

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

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

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

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

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

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

它仅将打印添加到原始代码。 但是现在您会更好地看到实际发生的情况。 请注意,通常在代码实际写入3时它通常只打印两个字符串。这是因为一旦所有写入goroutine完成,代码就会退出,但是结果会中途中断读取goroutine。 您可以通过在readChannel之前删除“ go”来“修复”它(这与在主函数中读取通道相同)。 然后,您将看到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()行,您的代码将等同于光电离的响应,并且在写入3时仅打印两个字符串。这是因为一旦所有写入器完成,代码就会退出(由swg.Wait()确保)。不要等待readChannel完成。

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

如果仅在readChannel调用之前删除“ go”,则等同于从主函数内部的通道读取。 打印3个字符串后,它将再次因死锁而崩溃,因为在所有写入器都已完成(且readChannel已读取所有写入内容)后,readChannel仍在读取。 棘手的一点是,此代码将永远无法到达swg.Wait()行,因为readChannel不会退出。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM