我正在研究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()
}
我正试图围绕这种微妙的差异 - 帮助表示赞赏。感谢。
答案 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都在等待;那是僵局。
您会注意到,如果您启动20个或更多工作人员而不是runtime.NumCPU()
,该程序将起作用。这是因为主要的goroutine可以通过input
发送它想要的所有内容,因为有足够的工作人员可以接收它们。
如果您将input <- i
循环放在另一个goroutine中,而不是所有这一切,就像在您的成功示例中一样,main
goroutine(其中ScheduleWorks
运行)可以继续并从output
开始阅读。因此,每次这个新的goroutine发送一个值时,工作者通过output
发送另一个,主goroutine获取此输出,并且worker可以接收另一个值。没有人等待,程序也成功了。
答案 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)
答案 2 :(得分:2)
请注意,对于这类任务,sync.WaitGroup
可能是完成此任务的另一种方法。也就是说,如果您需要在继续之前处理所有数据。
在同步包的文档中阅读:http://golang.org/pkg/sync#WaitGroup