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

时间:2017-03-05 07:09:22

标签: go concurrency goroutine

出于某种原因,一旦我开始通过我的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作为参考来修复它,但是当我删除缓冲区时它没有工作,这意味着我需要知道将提前向通道发送多少元素?

3 个答案:

答案 0 :(得分:5)

问题

根据您发布的代码建立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()
}

答案 1 :(得分:1)

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

答案 2 :(得分:1)

您无需知道尺寸即可使其正常工作。但你可能是为了彻底退出。这可能有点棘手,因为你的程序将在你的主函数退出后退出并且所有仍在运行的goroutine都会立即被杀死。

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

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

它只会在原始代码中添加打印功能。但现在你会更好地看到实际发生的事情。请注意,当代码实际写入时,它通常只会打印两个字符串。这是因为一旦所有写入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永远不会退出。

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

嗯,这应该涵盖所有基础。因此,请检查您的代码并查看哪些是您的。