私は並行Goライブラリに取り組んでいますが、結果が似ているゴルーチン間の同期の2つの異なるパターンに出くわしました。
待機グループを使用
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
を使用することの本当の利点は何ですか、および/またはそれがより良い場合の状況は何ですか?
2番目の例の正しさとは無関係に(コメントで説明されているように、あなたは思っていることをしていませんが、簡単に修正できます)、私は最初の例を理解しやすいと思う傾向があります。
今、私はチャンネルがより慣用的であるとさえ言いません。 Go言語の署名機能であるチャンネルは、可能な限りチャンネルを使用するのが慣用的であることを意味するべきではありません。 Goのイディオムは、最も簡単で理解しやすいソリューションを使用することです。ここで、WaitGroup
は、意味(主な機能は、実行されるワーカーのWait
ingです)とメカニック(ワーカーは、Done
であるときに通知します。
非常に特殊な場合を除き、ここでチャネルソリューションを使用することはお勧めしません。
チャネルのみを使用することに特にこだわりがある場合は、別の方法で実行する必要があります(@Not_a_Golferが指摘しているように例を使用すると、誤った結果が生成されます)。
1つの方法は、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)
}
ユースケースに依存します。各ジョブの結果を知る必要なしに並行して実行される1回限りのジョブをディスパッチしている場合、WaitGroup
を使用できます。しかし、ゴルーチンから結果を収集する必要がある場合は、チャネルを使用する必要があります。
チャンネルは両方の方法で機能するため、私はほとんど常にチャンネルを使用します。
別のメモでは、コメントで指摘されているように、チャンネルの例は正しく実装されていません。実行するジョブがこれ以上ないことを示すために、別のチャネルが必要です(1つの例は here です)。あなたの場合、あなたは事前に単語の数を知っているので、ただ一つのバッファリングされたチャンネルを使用し、閉じられたチャンネルを宣言することを避けるために一定の回数を受け取ることができます。
私はよくチャネルを使用して、エラーを生成する可能性のあるゴルーチンからエラーメッセージを収集します。以下に簡単な例を示します。
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
}
また、waitgroupを使用することをお勧めしますが、それでもチャンネルでそれをしたい場合は、以下でチャンネルの簡単な使用について言及します
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan string)
words := []string{"foo", "bar", "baz"}
go printWordrs(words, c)
for j := range c {
fmt.Println(j)
}
}
func printWordrs(words []string, c chan string) {
defer close(c)
for _, Word := range words {
time.Sleep(1 * time.Second)
c <- Word
}
}