TL;DR
複数のスレッドやプロセスが特定の地点に到達するまで待機し、全員がそろったところで一斉に次の処理へ進む同期手法。
仕組み
バリアは、管理する処理の数と、待機中の処理の数を持つ。処理がバリアに到達すると待機中の数が増え、それが管理する数に達したら、待っていた処理をすべて再開させる。同期したタイミングでカウンタは 0 に戻るので、同じバリアを何度でも使える。
WaitGroup との違い
Go の WaitGroup は、すべてのゴルーチンの完了を待つ。たとえば飛行機の出発では、乗客が全員空港のターミナルに到着するまで待ち、全員そろったら出発する。
一方、バリアは、すべての処理が任意の地点に到達するのを待ち、到達したら処理を再開させる。処理の終わりに限らず、好きな場所で同期できる。たとえば出発前の点検では、燃料補給、荷物の積み込み、乗客の搭乗がすべて終わるのを待ってから次に進む。
例
Go の標準パッケージにはバリアがないので、自分で実装する必要がある。条件変数(sync.Cond)を使うと、次のように書ける。
type Barrier struct {
// バリアで管理するゴルーチン数
size int
// 待機中のゴルーチン数
waitCount int
// 条件変数
cond *sync.Cond
}
func NewBarrier(size int) *Barrier {
return &Barrier{
size, 0, sync.NewCond(&sync.Mutex{}),
}
}
func (b *Barrier) Wait() {
b.cond.L.Lock()
b.waitCount++
if b.waitCount == b.size {
// すべてのゴルーチンがバリアに達した場合、全ゴルーチンを再開させる
b.waitCount = 0
b.cond.Broadcast()
} else {
b.cond.Wait()
}
}使い道の 1 つに正方行列の掛け算がある。行ごとにゴルーチンを立て、main ゴルーチンが行列を用意したらバリアで計算を始めさせ、すべての行の計算が終わるまでもう一度バリアで待つ。
func main() {
var matrixA, matrixB, result [matrixSize][matrixSize]int
barrier := NewBarrier(matrixSize + 1)
for row := range matrixSize {
go rowMultiply(&matrixA, &matrixB, &result, row, barrier)
}
for range 3 {
generateRandMatrix(&matrixA)
generateRandMatrix(&matrixB)
// ゴルーチンが計算できるようにバリアを開放
barrier.Wait()
// ゴルーチンが計算を完了するまで待機
barrier.Wait()
for i := range matrixSize {
fmt.Println(matrixA[i], matrixB[i], result[i])
}
fmt.Println("")
}
}
func generateRandMatrix(matrix *[matrixSize][matrixSize]int) {
for row := range matrixSize {
for col := range matrixSize {
matrix[row][col] = rand.Intn(10) - 5
}
}
}
func rowMultiply(
matrixA,
matrixB,
result *[matrixSize][matrixSize]int,
row int,
barrier *Barrier,
) {
for {
// main ゴルーチンが行列を読み込むまで待機
barrier.Wait()
for col := range matrixSize {
sum := 0
for i := range matrixSize {
sum += matrixA[row][i] * matrixB[i][col]
}
result[row][col] = sum
}
// すべての行の計算が終わるまで待機
barrier.Wait()
}
}自分の理解
Go言語で学ぶ並行プログラミング 他言語にも適用できる原則とベストプラクティス の図がとても分かりやすかった。