このドキュメントは、http://golang.org/doc/go_mem.htmlの翻訳です。


Version of June 10, 2011

はじめに
前事象
同期
初期化
ゴルーチンの作成
ゴルーチンの終了
チャネル通信
ロック
Once
同期の誤用

はじめに

このGo言語のメモリモデルでは、あるゴルーチンによって変数へ書き込まれた値を、他のゴルーチンから読み込む際に、書き込まれた値を参照できることを保証するための条件を規定します。

前事象

ひとつのゴルーチン内でのメモリへの読み書きは、プログラムで指定した実行順と同じように振舞わなくてはなりません。つまり、コンパイラとプロセッサが、ゴルーチン内の読み書きの順番を入れ替えられるのは、入れ替えによって言語仕様で定められたゴルーチンの振舞いが変更されないときだけです。この順番入れ替えの影響で、あるゴルーチン内では守られていた実行順が、他のゴルーチンからは違って見えることがあります。たとえば、あるゴルーチンで、a = 1; b = 2;を実行し、これを他のゴルーチンから観測すると、aの値が更新されるより先に、bの値が更新されるかも知れません。

ここで、読み書きにおける要件を規定するために、Go言語のプログラムにおけるメモリ操作の部分的な実行順を表す「前事象」という言葉を定義します。イベントe1が、イベントe2より「前事象」であるとき、e2はe1より「後事象」でもあります。また、e1がe2より「前事象」でも「後事象」でもなければ、e1とe2は「同時事象」です。

ひとつのゴルーチン内での、「前事象」の順番は、プログラムに記述された順番です。

次の2つの条件を満たすとき、「変数vへの書き込み」(w)した値を「変数vからの読み込み」(r)で参照できます。

  1. wは、rより「前事象」である。
  2. wより「後事象」かつ、rより「前事象」な「変数vへの書き込み」が存在しない。

「変数vからの読み込み」(r)が、特定の「変数vへの書き込み」(w)による値を参照することを保証するには、rが参照を許されている書き込みをwだけにします。つまり、次の2つの条件を満たすときに、rがwの値を参照することが保証されます。

  1. wは、rより「前事象」である。
  2. w以外の共有変数vへの書き込みは、wより「前事象」もしくは、rより「後事象」である。

この2つの条件は、wまたはrと「同時事象」な書き込みを許さないので、前述の2つの条件より影響力があります。

ひとつのゴルーチン内では、並列性は起きないので、上の2つの定義は同じで、読み込みrは、もっとも直近の書き込みwによってvへ書き込まれた値を参照します。複数のゴルーチンが共有変数vにアクセスするときは、読み込みが、適切な書き込みの値を参照するよう、同期イベントを使い、「前事象」状態を確立しなければなりません。

変数vの初期化(vの型のゼロ値による初期化)は、このメモリモデルにおける書き込みと同様の扱いです。

マシンのワードサイズより大きな値の読み書きは、ワードサイズ毎の複数の操作(順番は規定外)として行われます。

同期

初期化

プログラムの初期化は一つのゴルーチン内で行われ、初期化中に作成された新しいゴルーチンは、初期化が完了するまで実行は開始されません。

パッケージpで、パッケージqをインポートしているとき、p内のどの処理の開始より、qinit関数の完了は「前事象」です。

main.main関数の開始は、すべてのinit関数の完了よりも「後事象」です。

init関数内で作成されたすべてのゴルーチンの実行は、すべてのinit関数の完了よりも「後事象」です。

ゴルーチンの作成

新しいゴルーチンを起動するためのgoステートメントは、そのゴルーチンの実行開始よりも「前事象」です。

下のプログラムは、これの例です。

var a string

func f() {
	print(a)
}

func hello() {
	a = "hello, world"
	go f()
}

helloの呼び出しのすこし後(おそらく、helloから戻ったあと)で、"hello, world"と出力が行われます。

ゴルーチンの終了

ゴルーチンの終了が、プログラム中のどの事象より前に起きるかは保証されません。次のプログラムで例示します。

var a string

func hello() {
	go func() { a = "hello" }()
	print(a)
}

このaへの代入は、どの事象とも同期していないため、他のゴルーチンからの参照は保証されません。ある種のコンパイラの最適化によって、goステートメント全体が取り除かれる可能性も実際にあり得ます。

あるゴルーチンで行った処理を、他のゴルーチンから参照する必要があるときは、ロックやチャネル通信といった同期メカニズムを使用して、処理される順序を取り決めてください。

チャネル通信

チャネルによる通信は、ゴルーチン間で同期をとるための主な手段です。あるチャネルのへの送信はそれぞれ、そのチャネルからの受信と対応します。この送信と受信は通常、異なるゴルーチン間で行われます。

チャネルへの送信は、そのチャネルからの受信完了よりも「前事象」です。

var c = make(chan int, 10)
var a string

func f() {
	a = "hello, world"
	c <- 0
}

func main() {
	go f()
	<-c
	print(a)
}

上のプログラムでは、"hello, world"と出力されることが保証されます。aへの書き込みは、cの送信よりも「前事象」であり、この送信は、cの受信完了よりも「前事象」で、受信完了は、printよりも「前事象」です。

チャネルがクローズされたことにより、受信側がゼロ値を受け取るときは、この受信よりチャネルのクローズは「前事象」です。

先ほどの例の、c <- 0close(c)に置き換えても、プログラムの振る舞いが変わらないことは保証されます。

バッファリングされないチャネルからの受信は、そのチャネルの送信完了よりも「前事象」です。

次のプログラムは、前のプログラムと似ていますが、送信と受信のステートメントを入れ替え、さらにバッファリングしないチャネルを使用しています。

var c = make(chan int)
var a string

func f() {
	a = "hello, world"
	<-c
}
func main() {
	go f()
	c <- 0
	print(a)
}

このプログラムも同様に、"hello, world"と出力されることが保証されます。aへの書き込みは、cからの受信よりも「前事象」であり、この受信は、cへの送信完了よりも「前事象」で、送信完了は、printよりも「前事象」です。

このチャネルがバッファリングされているとき(例:c = make(chan int, 1))は、このプログラムで"hello, world"と出力されることは保証されません。(おそらく、空文字列が出力されますが、ここで"goodbye, universe"と出力されることもなく、またクラッシュすることもありません。)

ロック

syncパッケージには、sync.Mutexおよび、sync.RWMutexという2つのロック用のデータ型が実装されています。

sync.Mutexもしくは、sync.RWMutex型の変数lがあり、n < mであるとき、n番目のl.Unlock()の呼び出しは、m番目のl.Lock()からの復帰よりも「前事象」です。

var l sync.Mutex
var a string

func f() {
	a = "hello, world"
	l.Unlock()
}

func main() {
	l.Lock()
	go f()
	l.Lock()
	print(a)
}

上のプログラムでは、"hello, world"と出力されることが保証されます。最初のl.Unlock()の呼び出し(f関数内)は、2番目のl.Lock()(main関数内)からの復帰よりも「前事象」であり、これらは、printよりも「前事象」です。

sync.RWMutex型の変数lに対して、l.RLockを呼び出すときは、常に次の条件を満たすnが存在します。この条件は、l.RLockの呼び出し(からの復帰)が、n番目のl.Unlockの呼び出しよりも「後事象」であり、かつそれに対応するl.RUnlockの呼び出しが、n+1番目のl.Lockの呼び出しよりも「前事象」であることです。

Once

syncパッケージは、複数のゴルーチンにおける安全な初期化メカニズムをOnce型を用いることにより提供します。複数のスレッドから、特定のf関数に対してonce.Do(f)を実行できますが、そのうちのひとつだけがf()を実行し、それ以外の呼び出しは、f()から戻るまでの間、ブロックされます。

once.Do(f)から、一度だけ呼び出されるf()からの復帰は、すべてのonce.Do(f)呼び出しからの復帰よりも「前事象」です。

var a string
var once sync.Once

func setup() {
	a = "hello, world"
}

func doprint() {
	once.Do(setup)
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

上のプログラムのtwoprintの呼び出しによって、"hello, world"が2回出力されます。また、setupは、twoprintの初回の呼び出しの際に一度だけ実行されます。

同期の誤用

「読み込み」(r)が、「同時事象」である「書き込み」(w)によって書き込まれた値を参照するときは注意が必要です。たとえ、うまくいったとしても、rより「後事象」な読み込みが、wより「前事象」な書き込みを参照してしまっては無意味です。

var a, b int

func f() {
	a = 1
	b = 2
}

func g() {
	print(b)
	print(a)
}

func main() {
	go f()
	g()
}

このプログラムで、g20を出力することも起こり得ます。

この現象によって、一般的なイディオムのいくつかが役に立たなくなります。

ダブルチェックロッキングは、同期によるオーバヘッドを軽減するためのイディオムですが、次のように、正しくないtwoprintプログラムが記述されてしまうかも知れません。

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func doprint() {
	if !done {
		once.Do(setup)
	}
	print(a)
}

func twoprint() {
	go doprint()
	go doprint()
}

doprint内で、doneへ書き込まれた値を参照すること(aへ書き込まれた値を参照することも同じ)は保証されません。このバージョンでは、"hello, world"ではなく、(予期していない)空文字列が出力されることがあります。

もう一つの、誤りとなるイディオムは、値のビジーウェイトと呼ばれるもので、次のようになります。

var a string
var done bool

func setup() {
	a = "hello, world"
	done = true
}

func main() {
	go setup()
	for !done {
	}
	print(a)
}

これも前と同様に、doneへ書き込まれた値をmain内で参照すること(aへ書き込まれた値を参照することも同じ)は保証されないため、このプログラムでも空文字列が出力されることがあります。更に悪いことに、この2つのスレッド間には同期イベントがないため、doneへ書き込まれた値がmainから参照できるとは、いつまでも保証されません。よって、このmain内のループが終了することも保証できません。

先ほどのプログラムの変形である、次のプログラムには、より微妙な問題点が存在します。

type T struct {
	msg string
}

var g *T

func setup() {
	t := new(T)
	t.msg = "hello, world"
	g = t
}

func main() {
	go setup()
	for g == nil {
	}
	print(g.msg)
}

この例のmainで、g != nilとなって、ループを抜けたとしても、そこでg.msgから初期化済みの値が参照できるとは保証されません。

これらの例で取り上げた問題を解決するためには、明示的な同期を使うようにしてください。