去 - 为什么安排goroutine背景工作者也需要自己的goroutine?

时间:2014-03-24 10:35:42

标签: design-patterns concurrency go goroutine

我正在研究Go的一些并发模式。我查看了使用goroutine和输入/输出通道实现后台工作程序,并注意到当我将新作业发送到接收通道(基本上将新作业排入队列)时,我必须在goroutine中执行此操作,否则调度会变得混乱。含义:

崩溃了:

for _, jobData := range(dataSet) {
    input <- jobData
}

这有效:

go func() {
    for _, jobData := range(dataSet) {
        input <- jobData
    }
}()

对于更具体的内容,我使用了一些无意义的代码(here it is in go playground):

package main

import (
    "log"
    "runtime"
)

func doWork(data int) (result int) {
    // ... some 'heavy' computation
    result = data * data
    return
}

// do the processing of the input and return
// results on the output channel
func Worker(input, output chan int) {
    for data := range input {
        output <- doWork(data)
    }
}

func ScheduleWorkers() {

    input, output := make(chan int), make(chan int)

    for i := 0 ; i < runtime.NumCPU() ; i++ {
        go Worker(input, output)
    }

    numJobs := 20

    // THIS DOESN'T WORK
    // and crashes the program
    /*
    for i := 0 ; i < numJobs ; i++ {
        input <- i
    }
    */

    // THIS DOES
    go func() {
        for i := 0 ; i < numJobs ; i++ {
            input <- i
        }
    }()

    results := []int{}
    for i := 0 ; i < numJobs ; i++ {
        // read off results
        result := <-output
        results = append(results, result)
        // do stuff...
    }

    log.Printf("Result: %#v\n", results)
}

func main() {
    ScheduleWorkers()
}

我正试图围绕这种微妙的差异 - 帮助表示赞赏。感谢。

3 个答案:

答案 0 :(得分:6)

您的ScheduleWorks函数在主goroutine(即运行main()函数的函数,程序启动的函数)中发送,通过input发送一个值。 Worker收到它,并通过output发送另一个值。但是当时没有人从output收到,因此程序无法继续,主goroutine会将下一个值发送给另一个Worker

为每个工人重复这个推理。您有runtime.NumCPU()名工作人员,可能小于numJobs。让我们说runtime.NumCPU() == 4,所以你有4个工人。最后,您已成功发送4个值,每个值为1个Worker。由于没有人从output开始阅读,因此所有工作人员都在忙于发送,因此他们无法通过input接受更多数据,因此第五个input <- i将会挂起。在这一点上,每个goroutine都在等待;那是僵局。

enter image description here

您会注意到,如果您启动20个或更多工作人员而不是runtime.NumCPU(),该程序将起作用。这是因为主要的goroutine可以通过input发送它想要的所有内容,因为有足够的工作人员可以接收它们。

如果您将input <- i循环放在另一个goroutine中,而不是所有这一切,就像在您的成功示例中一样,main goroutine(其中ScheduleWorks运行)可以继续并从output开始阅读。因此,每次这个新的goroutine发送一个值时,工作者通过output发送另一个,主goroutine获取此输出,并且worker可以接收另一个值。没有人等待,程序也成功了。

enter image description here

答案 1 :(得分:2)

这是因为Go中的所有内容都默认为阻止。

当您在无缓冲通道上发送第一个值时,它会一直阻塞,直到接收器从通道中取出该值。

可以通过添加“容量”来缓冲频道。

例如:

make(chan int, 20) // Make a buffered channel of int with capacity 20

来自the Go spec

  

容量(以元素数量)设置通道中缓冲区的大小。如果容量大于零,则通道是异步的:如果缓冲区未满(发送)或非空(接收),则通信操作成功而不阻塞,并且按发送顺序接收元素。如果容量为零或不存在,则仅当发送方和接收方都准备就绪时,通信才会成功。

您可以使用缓冲通道而不是无缓冲通道来使原始功能正常工作,但将函数调用包装在goroutine中可能是一种更好的方法,因为它实际上是并发的。

来自Effective Go(完整阅读本文档!这可能是Stack Overflow上Go回答中链接最多的文档):

  

接收器始终阻塞,直到有数据要接收为止。如果通道未缓冲,则发送方将阻塞,直到接收方收到该值。如果通道有缓冲区,则发送方仅阻塞,直到将值复制到缓冲区为止;如果缓冲区已满,则表示等待某个接收方检索到一个值。

如果您使用缓冲频道,那么您只需填充频道,继续前进,然后再将其耗尽。不是同时发生的。

示例:

更改

input, output := make(chan int), make(chan int)

input, output := make(chan int, 20), make(chan int, 20)

Playground

答案 2 :(得分:2)

请注意,对于这类任务,sync.WaitGroup可能是完成此任务的另一种方法。也就是说,如果您需要在继续之前处理所有数据。

在同步包的文档中阅读:http://golang.org/pkg/sync#WaitGroup