我试图跟随Rob Pike的例子来谈谈“并发不是并行性”#39;并做了这样的事情: 我作为工作人员从输入通道读取,执行一些处理然后通过输出通道发送结果,我开始了很多例程。
然后我开始另一个go例程,从一些源读取数据并通过其输入通道将其发送给工作人员。 最后,我想迭代输出通道中的所有结果并对它们做一些事情。 问题在于,由于工作在工人之间分配,我不知道所有工人何时完成,所以我可以停止询问输出渠道以获得更多结果,我的程序可以正常结束。
了解员工何时将结果发送到输出渠道的最佳做法是什么?
答案 0 :(得分:3)
我个人喜欢使用sync.WaitGroup
。 waitgroup是一个同步计数器,有三种方法 - Wait()
,Done()
和Add()
。你做的是增加waitgroup的计数器,将它传递给工作人员,让他们在完成后调用Done()
。然后你只需阻塞另一端的等待组,并在它们全部完成时关闭输出通道,导致输出处理器退出。
基本上:
// create the wait group
wg := sync.WaitGroup{}
// this is the output channel
outchan := make(chan whatever)
// start the workers
for i := 0; i < N; i++ {
wg.Add(1) //we increment by one the waitgroup's count
//the worker pushes data onto the output channel and calls wg.Done() when done
go work(&wg, outchan)
}
// this is our "waiter" - it blocks until all workers are done and closes the channel
go func() {
wg.Wait()
close(outchan)
}()
//this loop will exit automatically when outchan is closed
for item := range outchan {
workWithIt(item)
}
// TADA!
答案 1 :(得分:0)
我可以先澄清你的术语:对渠道末端的误解可能会导致问题。你询问“输出通道”和“输入通道”。哪有这回事;只有频道。
每个通道都有两个结束:输出(写入)结束,输入(读取)结束。我会假设这就是你的意思。
现在回答你的问题。
采取最简单的情况:您只有一个发件人goroutine写入一个频道,并且您只有一个工作者goroutine从另一端读取,并且该频道有零缓冲。发件人goroutine将阻止它写入每个项目,直到该项目被消费。通常这种情况在第一次发生很快。一旦第一个项目传递给工作人员,工作人员将忙碌,发件人必须等待第二个项目可以传递。因此,乒乓效应如下:作者或读者将忙,但不是两者兼而有之。在Rob Pike描述的意义上,goroutines将是并发,但并不总是实际并行执行 。
如果您有许多工作人员goroutines从频道读取(并且其输入端由所有人共享),发件人最初可以将一个项目分配给每个工作人员,但是它必须在工作时等待(类似于上面描述的乒乓球案例)。最后,当发件人发送了所有项目后,它已完成其工作。但是,读者可能还没有完成他们的工作。有时我们关心发件人提前完成,有时我们不关心。知道何时发生这种情况最容易通过WaitGroup完成(参见 Not_a_Golfer 的答案和my answer to a related question)。
有一个稍微复杂的替代方案:您可以使用返回通道来完成信号,而不是WaitGroup
。这并不难,但在这种情况下,WaitGroup
是首选,更简单。
如果信道要包含一个缓冲区,则发送方发送其最后一个项目的点会更快发生。在极限情况下,通道每个工人有一个缓冲空间;这将允许发件人很快完成,然后可能继续使用其他东西。 (任何比这更缓冲都会浪费)。
发送方的这种分离允许完全异步的行为模式,受到使用其他技术堆栈的人的喜爱(Node-JS和JVM spring)。与他们不同,Go不会需要你这样做,但你可以选择。
回到90年代早期,作为批量同步并行(BSP)策略工作的副作用,Leslie Valiant证明有时非常简单的同步策略可以很便宜。关键因素是需要足够的并行松弛(a.k.a。过剩的并行性)来保持处理器内核的繁忙。这意味着必须有足够的其他工作要做,以便在一段时间内阻止任何特定的goroutine无关紧要。奇怪的是,这可能意味着使用较少数量的goroutine可能需要更多关心,而不是使用更大的数字。
理解过度并行的影响是有用的:如果整个网络具有过多的并行性,通常没有必要付出额外的努力来使所有内容异步,因为CPU内核会以任何方式忙碌。
因此,虽然了解如何等到发件人完成后很有用,但更大的应用程序可能不需要您以同样的方式关注。
作为最后一个脚注,WaitGroup
是BSP中使用的意义上的障碍。通过组合障碍和渠道,您可以使用BSP和CSP。
答案 2 :(得分:0)
var Z = "Z"
func Loop() {
sc := make(chan *string)
ss := make([]string, 0)
done := make(chan struct{}, 1)
go func() {
//1 QUERY
slice1 := []string{"a", "b", "c"}
//2 WG INIT
var wg1 sync.WaitGroup
wg1.Add(len(slice1))
//3 LOOP->
loopSlice1(slice1, sc, &wg1)
//7 WG WAIT<-
wg1.Wait()
sc <- &Z
done <- struct{}{}
}()
go func() {
var cc *string
for {
cc = <-sc
log.Infof("<-sc %s", *cc)
if *cc == Z {
break
}
ss = append(ss, *cc)
}
}()
<-done
log.Infof("FUN: %#v", ss)
}
func loopSlice1(slice1 []string, sc chan *string, wg1 *sync.WaitGroup) {
for i, x := range slice1 {
//4 GO
go func(n int, v string) {
//5 WG DONE
defer wg1.Done()
//6 DOING
//[1 QUERY
slice2 := []string{"X", "Y", "Z"}
//[2 WG INIT
var wg2 sync.WaitGroup
wg2.Add(len(slice2))
//[3 LOOP ->
loopSlice2(n, v, slice2, sc, &wg2)
//[7 WG WAIT <-
wg2.Wait()
}(i, x)
}
}
func loopSlice2(n1 int, v1 string, slice2 []string, sc chan *string, wg2 *sync.WaitGroup) {
for j, y := range slice2 {
//[4 GO
go func(n2 int, v2 string) {
//[5 WG DONE
defer wg2.Done()
//[6 DOING
r := fmt.Sprintf("%v%v %v,%v", n1, n2, v1, v2)
sc <- &r
}(j, y)
}
}