【概要】
この記事は DOG という初心者向けのGo言語の勉強会に用いた資料をQiita版として落とし込んだものです。
なので初心者向けの内容となっておりますので、あしからず。
またDOGの時の資料はこちらになります。
【ゴルーチン】
Go言語にはゴルーチンという機能があり、ゴルーチンを用いることで関数・メソッド・クロージャを並行処理として呼び出すことが可能です。ゴルーチンを使う場合は go
キーワードを用います。
- 関数の場合
go logging("Hello, %s\n", name)
- メソッドの場合
go logger.Printf("Hello, %s\n", name)
- クロージャの場合
go func() {
log.Printf("Hello, %s\n", name)
}()
【並行処理の実行順番】
- 問題
唐突ですが問題です。以下のプログラムを実行すると何が出力されるでしょうか?
func main() {
var data int
go func() {
data++
}()
if data == 0 {
fmt.Printf("%v\n", data)
}
}
選択肢) A. 0 B. 1 C. 何も出力されない
答えは、「時と場合による」でした。並行処理では実行順番の保証がないためdata変数にアクセスする順番が変わることがあります。それぞれの結果の場合の実行順番は以下の通りです。
-
0と出力される場合
if data == 0
fmt.Printf("%v\n", data)
data++
-
1と出力される場合
if data == 0
data++
fmt.Printf("%v\n", data)
-
何も出力されない場合
data++
if data == 0
- 競合
このように順番が保証されていない状態を競合状態、特に変数に対する競合をデータ競合と言います。
並行処理に慣れていないエンジニアからするとプログラムは上から順番に実行されるという先入観があるため、知らず知らずに競合が発生するプログラムを実装し、バグを生み出しかねません。
【並行処理における実行順番の保証】
- スリープによる順番保証
唐突ですが再び問題です。以下のプログラムを実行すると何が出力されるでしょうか?
func main() {
var data int
go func() {
time.Sleep(1 * time.Second) // 1秒間スリープする
data++
}()
if data == 0 {
fmt.Printf("%v\n", data)
}
}
選択肢) A. 0 B. 1 C. 何も出力されない
答えは、「0が出力されやすくなった」です。data++
の前で1秒間スリープするのだから、絶対に0が表示されるでしょ!と思えるかもしれませんが、何らかの要因により if data == 0
の比較処理に1秒以上要してしまった場合は何も出力されません。そんなことほとんど起きませんが、確実に起きないとは言えないためスリープでは順番の保証が行えず、あくまで順番が保たれやすくなっただけです。
- アトミック性
他のスレッドには割り込ませずに、今行なっている変更操作が確実に終わった後に実行される性質、言い換えるならばこれ以上処理を分解できない性質をアトミック性と言います。アトミック性はスレッドセーフの実現に重要な要素となります。
例えば、 i++
はアトミックでしょうか?
答えは、「アトミックではない」です。
i++
は参照、計算、代入の3つに分解することが可能です。
参照している途中、計算している途中、代入している途中に他のスレッドからiを参照されるといったことはありませんが、参照してから計算するまでの間に他のスレッドからiを参照されることはあり得ます。
- sync.Mutex
i++
はアトミックではないと説明しましたが、syncパッケージのMutexを使うことでアトミックとすることが可能です。
Mutexはロックされたら、アンロックされるまで他の処理にロックを渡さないという機能です。
以下の例をご覧ください。
func main() {
var data int
var mu sync.Mutex
go func() {
mu.Lock()
data++
mu.Unlock()
}()
mu.Lock()
if data == 0 {
fmt.Printf("%v\n", data)
}
mu.Unlock()
}
Mutexがロックしているため、 data++
をしている途中に if data == 0
が行われるといったことは無くなりました。
これで順番を保証することができました。めでたしめでたし。
と言いたいのですが、まだ保証しきれていません。data++
が先に実行されるか、 if data == 0
が先に実行されるかは保証されていないため、出力結果は「0」と「何も出力されない」の2通りあります。
【考察】
func main() {
var data int
go func() {
data++
}()
if data == 0 {
fmt.Printf("%v\n", data)
}
}
今回は紹介していませんがsyncパッケージによりMutexなどの同期処理が提供されています。が、そもそもこのプログラムを並行処理させる必要はありますでしょうか?
流石に今回の例は極端ではございますが、並行処理で実行すべき箇所、そうでない箇所を見極める能力が問われてくるのではないでしょうか?
【ここがムズいよ並行処理】
- 副作用がある場合、順番を保証しないとバグの温床となる
- syncパッケージを活用してアトミック性を意識する必要がある
- 並行処理で実装すべきか見極める必要がある