我正在使用并发Go库,我偶然发现了结果相似的goroutine之间的两种不同的同步模式:
使用Waitgroup
var wg sync.WaitGroup
func main() {
words := []string{ "foo", "bar", "baz" }
for _, word := range words {
wg.Add(1)
go func(word string) {
time.Sleep(1 * time.Second)
defer wg.Done()
fmt.Println(word)
}(word)
}
// do concurrent things here
// blocks/waits for waitgroup
wg.Wait()
}
使用频道
func main() {
words = []string{ "foo", "bar", "baz" }
done := make(chan bool)
defer close(done)
for _, word := range words {
go func(word string) {
time.Sleep(1 * time.Second)
fmt.Println(word)
done <- true
}(word)
}
// Do concurrent things here
// This blocks and waits for signal from channel
<-done
}
我被告知sync.WaitGroup
性能略高,我看到它被普遍使用。但是,我发现渠道更加惯用。使用sync.WaitGroup
超过渠道的真正优势和/或什么样的情况可能会更好?
答案 0 :(得分:29)
独立于第二个例子的正确性(正如评论中所解释的那样,你没有按照自己的想法行事,但它很容易修复),我倾向于认为第一个例子更容易理解。
现在,我甚至不会说渠道更加惯用。作为Go语言的标志性特征的频道不应该意味着尽可能使用它们是惯用的。 Go中惯用的是使用最简单易懂的解决方案:在这里,'#'.__mul__(10) = '##########'
传达意义(你的主要功能是WaitGroup
为工人完成)和机制(工作人员在Wait
)时会通知。
除非您处于非常具体的情况,否则我不建议您在此处使用频道解决方案。
答案 1 :(得分:2)
如果你对仅使用频道特别感兴趣,那么它需要以不同的方式完成(如果我们使用你的例子,就像@Not_a_Golfer指出的那样,它会产生不正确的结果)。
一种方法是创建int类型的通道。在工作进程中,每次完成作业时都会发送一个数字(这也可以是唯一的作业ID,如果您希望可以在接收器中跟踪它)。
在接收者主要例行程序中(将知道提交的作业的确切数量) - 在通道上进行范围循环,直到提交的作业数量没有完成,并且在所有作业完成后退出循环完成了。如果您想跟踪每个作业的完成情况(如果需要可能会做某些事情),这是一个很好的方法。
以下是供您参考的代码。减少totalJobsLeft将是安全的,因为它只会在通道的范围循环中完成!
//This is just an illustration of how to sync completion of multiple jobs using a channel
//A better way many a times might be to use wait groups
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
comChannel := make(chan int)
words := []string{"foo", "bar", "baz"}
totalJobsLeft := len(words)
//We know how many jobs are being sent
for j, word := range words {
jobId := j + 1
go func(word string, jobId int) {
fmt.Println("Job ID:", jobId, "Word:", word)
//Do some work here, maybe call functions that you need
//For emulating this - Sleep for a random time upto 5 seconds
randInt := rand.Intn(5)
//fmt.Println("Got random number", randInt)
time.Sleep(time.Duration(randInt) * time.Second)
comChannel <- jobId
}(word, jobId)
}
for j := range comChannel {
fmt.Println("Got job ID", j)
totalJobsLeft--
fmt.Println("Total jobs left", totalJobsLeft)
if totalJobsLeft == 0 {
break
}
}
fmt.Println("Closing communication channel. All jobs completed!")
close(comChannel)
}
答案 2 :(得分:1)
这取决于用例。如果您要调度一次性作业以便并行运行而无需了解每项作业的结果,那么您可以使用WaitGroup
。但是如果你需要从goroutines收集结果,那么你应该使用一个频道。
由于频道双向工作,我几乎总是使用频道。
另一方面,正如评论中指出的那样,您的频道示例未正确实施。您需要一个单独的通道来指示没有其他工作要做(一个例子是here)。在您的情况下,由于您事先知道了单词的数量,因此您可以使用一个缓冲通道并接收固定次数以避免声明关闭通道。
答案 3 :(得分:1)
我经常使用渠道从goroutine收集可能产生错误的错误消息。这是一个简单的示例:
func couldGoWrong() (err error) {
errorChannel := make(chan error, 3)
// start a go routine
go func() (err error) {
defer func() { errorChannel <- err }()
for c := 0; c < 10; c++ {
_, err = fmt.Println(c)
if err != nil {
return
}
}
return
}()
// start another go routine
go func() (err error) {
defer func() { errorChannel <- err }()
for c := 10; c < 100; c++ {
_, err = fmt.Println(c)
if err != nil {
return
}
}
return
}()
// start yet another go routine
go func() (err error) {
defer func() { errorChannel <- err }()
for c := 100; c < 1000; c++ {
_, err = fmt.Println(c)
if err != nil {
return
}
}
return
}()
// synchronize go routines and collect errors here
for c := 0; c < cap(errorChannel); c++ {
err = <-errorChannel
if err != nil {
return
}
}
return
}
答案 4 :(得分:1)
WaitGroup
的主要优势是简单。
通道可以是缓冲的,也可以是非缓冲的,并且可以承载一条消息或仅一个信号(没有消息-空通道),因此存在许多不同的用例,并且
“ WaitGroup等待goroutine的集合完成。 主要的goroutine调用Add来设置数量 等待的goroutines。然后每个goroutines 运行并在完成后调用完成。与此同时, 等待可以用来阻止,直到所有goroutine完成。”
让我们做一个基准:
TLDR:
在the same code中使用sync.WaitGroup
(与完成频道的方式相同)比缓冲完成频道(对于以下基准测试)快一点(与9%
相比): br />
695 ns/op
与758 ns/op
。
对于无缓冲完成的频道,使用sync.WaitGroup
的速度更快(2x or more
)-由于无缓冲的频道同步(对于以下基准测试):
722 ns/op
与2343 ns/op
。
基准(使用go version go1.14.7 linux/amd64
)
var done = make(chan struct{}, 1_000_000)
使用benchtime
命令:
go test -benchtime=1000000x -benchmem -bench .
# BenchmarkEvenWaitgroup-8 1000000 695 ns/op 4 B/op 0 allocs/op
# BenchmarkEvenChannel-8 1000000 758 ns/op 50 B/op 0 allocs/op
var done = make(chan struct{})
使用此命令:
go test -benchtime=1000000x -benchmem -bench .
# BenchmarkEvenWaitgroup-8 1000000 722 ns/op 4 B/op 0 allocs/op
# BenchmarkEvenChannel-8 1000000 2343 ns/op 520 B/op 1 allocs/op
Code:
package main
import (
"sync"
)
func main() {
evenWaitgroup(8)
}
func waitgroup(n int) {
select {
case ch <- n: // tx if channel is empty
case i := <-ch: // rx if channel is not empty
// fmt.Println(n, i)
_ = i
}
wg.Done()
}
func evenWaitgroup(n int) {
if n%2 == 1 { // must be even
n++
}
for i := 0; i < n; i++ {
wg.Add(1)
go waitgroup(i)
}
wg.Wait()
}
func channel(n int) {
select {
case ch <- n: // tx if channel is empty
case i := <-ch: // rx if channel is not empty
// fmt.Println(n, i)
_ = i
}
done <- struct{}{}
}
func evenChannel(n int) {
if n%2 == 1 { // must be even
n++
}
for i := 0; i < n; i++ {
go channel(i)
}
for i := 0; i < n; i++ {
<-done
}
}
var wg sync.WaitGroup
var ch = make(chan int)
var done = make(chan struct{}, 1000000)
// var done = make(chan struct{})
注意:为已缓冲和未缓冲的已完成频道基准测试切换注释:
var done = make(chan struct{}, 1000000)
// var done = make(chan struct{})
main_test.go
文件:
package main
import (
"testing"
)
func BenchmarkEvenWaitgroup(b *testing.B) {
evenWaitgroup(b.N)
}
func BenchmarkEvenChannel(b *testing.B) {
evenChannel(b.N)
}
答案 5 :(得分:0)
同时建议使用waitgroup,但仍然想用通道进行,然后在下面我提到一个简单的使用频道
0