Skip to content

Latest commit

 

History

History

step04

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

STEP 4: syncパッケージを使う

チャネルを使わないゴールーチン間のやり取り

STEP 3では、ゴールーチン間でデータのやりとりを行う場合は、 データの競合を避けるためにチャネルを利用するという説明を行いました。

しかし、データの競合を避けるためには、チャネルを使わずロックをとって排他制御を行う方法もあります。 syncパッケージはゴールーチンを跨いだロックなどの便利な機能を提供するパッケージです。

例えば、次のようにsync.Mutexを用いることでロックを取ることができます。

var (
	count int
	mu    sync.Mutex
)

done := make(chan bool)
go func() {
	for i := 0; i < 10; i++ {
		mu.Lock()
		count++
		mu.Unlock()
		time.Sleep(100 * time.Millisecond) // 10[ms]スリープ
	}
	done <- true
}()

go func() {
	for i := 0; i < 10; i++ {
		mu.Lock()
		count++
		mu.Unlock()
		time.Sleep(100 * time.Millisecond) // 10[ms]スリープ
	}
	done <- true
}()

<-done
<-done
fmt.Println(count)

sync.MutexLockメソッドでロックを取り、Unlockメソッドでロックを解除します。 すでにロックが掛かっているMutexに対してLockメソッドを呼び出そうとすると、Unlockメソッドが呼び出されるまで処理がブロックされます。

Unlockメソッドはdeferで呼び出すこともありますが、forや再帰呼び出しなどでデッドロックを起こす可能性があるため注意が必要です。

ゴールーチンの待ち合わせ

複数のゴールーチンの処理を待って次の処理に移りたい場合があります。 例えば、お湯を沸かすことと豆を挽くことは並列で行っても問題ありませんが、 コーヒーを淹れるためには、お湯と挽いた豆が揃っている必要があります。

sync.WaitGroupはゴールーチンの待ち合わせを行う機能を提供しています。 使い方はとてもシンプルです。 Waitメソッドで複数のゴールーチンの処理を待ち合わせることができ、 Addメソッドで追加した数だけDoneメソッドが呼ばれるまで処理がブロックされます。

例えば、次の例ではwg.Add(1)が2回実行されているため、wg.Done()が2回実行されるまで wg.Wait()で処理をブロックします。

var (
	count int
	mu    sync.Mutex
)

var wg sync.WaitGroup
wg.Add(1)
go func() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		mu.Lock()
		count++
		mu.Unlock()
		time.Sleep(100 * time.Millisecond) // 10[ms]スリープ
	}
	done <- true
}()

wg.Add(1)
go func() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		mu.Lock()
		count++
		mu.Unlock()
		time.Sleep(100 * time.Millisecond) // 10[ms]スリープ
	}
}()

// 2つのゴールーチンの処理が終わるまで待つ
wg.Wait() 
fmt.Println(count)

プログラムの改造

チャネルを使わないようにコーヒーを淹れるプログラムを改造してみましょう。

boil関数、grind関数、brew関数の処理結果はチャネル経由ではなく戻り値で受け取ります。 受け取った戻り値を変数に足す必要がありますが、そのまま足すと競合が起きるためロックを取って足す必要があります。

boil関数とgrind関数の処理は並列で実行しても問題ないため、それぞれゴールーチンで実行し、1つのsync.WaitGroupで待ち合わせすることにします。

brew関数の処理はゴールーチンで呼ばれますが、別のsync.WaitGroupでコーヒーが全て淹れ終わるまで待つことにしましょう。

実行とトレースデータの表示

TODOを埋めると次のコマンドで実行することができます。

$ go run main.go

trace.outというトレース情報を記録したファイルが出力されるため、次のコマンドで結果を表示します。

$ go tool trace trace.out

ブラウザが開くので、User-defined tasks -> Count -> Task 1の順番で開くと次のような結果が表示されれば成功です。

実行時間はSTEP 3と大きく変わりません。 同じようにboilgrindが並列に処理され、その後にgrindが処理されています。 しかし、boilgrindのバーの長さが同じではなくなっています。

STEP 3ではboilからのデータの送信をすべて待った後に、grindからのデータを受け取っていましたが、 今回はsync.WaitGroupboilgrindも関係なく待っていたので、grindの方がboilの長さに引っ張られずに終了したためです。