同期

初期化

プログラムの初期化は単一のゴルーチンで実行されますが、そのゴルーチンは他のゴルーチンを作成することがあり、これらは並行して実行されます。

パッケージ p がパッケージ q をインポートする場合、qinit 関数の完了は、p の任意の関数の開始よりも前に発生します。

すべての init 関数の完了は、関数 main.main の開始よりも前に同期されます。

ゴルーチン作成

新しいゴルーチンを開始する go 文は、ゴルーチンの実行開始よりも前に同期されます。

例えば、このプログラムでは:

var a string

func f() {
    print(a)
}

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

hello を呼び出すと、将来のある時点で "hello, world" が出力されます(おそらく hello が戻った後)。

ゴルーチン破棄

ゴルーチンの終了は、プログラム内の任意のイベントよりも前に同期されることは保証されません。例えば、このプログラムでは:

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" を出力することが保証されません。(空文字列を出力したり、クラッシュしたり、他のことをするかもしれません。)

容量 C のチャネルでの k 番目の受信は、そのチャネルでの k+C 番目の送信の完了よりも前に同期されます。

この規則は、前の規則をバッファーされたチャネルに一般化します。カウンティングセマフォをバッファーされたチャネルでモデル化することを可能にします:チャネル内のアイテム数はアクティブな使用数に対応し、チャネルの容量は同時使用の最大数に対応し、アイテムの送信はセマフォの取得、アイテムの受信はセマフォの解放に対応します。これは、並行性を制限するための一般的なイディオムです。

このプログラムは、作業リストのすべてのエントリに対してゴルーチンを開始しますが、ゴルーチンは limit チャネルを使用して調整し、最大3つが同時に作業関数を実行することを保証します。

var limit = make(chan int, 3)

func main() {
    for _, w := range work {
        go func(w func()) {
            limit <- 1
            w()
            <-limit
        }(w)
    }
    select{}
}

ロック

sync パッケージは、2つのロックデータ型、sync.Mutexsync.RWMutex を実装します。

任意の sync.Mutex または sync.RWMutex 変数 ln < m について、l.Unlock() の呼び出し n は、l.Lock() の呼び出し m が戻るよりも前に同期されます。

このプログラム:

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 内)は、l.Lock() の2番目の呼び出し(main 内)が戻るよりも前に同期され、これは print よりも前に順序付けられます。

sync.RWMutex 変数 l での l.RLock への任意の呼び出しについて、l.Unlockn 番目の呼び出しが l.RLock からの戻りよりも前に同期され、一致する l.RUnlock の呼び出しが l.Lock への呼び出し n+1 の戻りよりも前に同期されるような n が存在します。

l.TryLock(または l.TryRLock)の成功した呼び出しは、l.Lock(または l.RLock)の呼び出しと同等です。失敗した呼び出しは、同期効果を全く持ちません。メモリモデルに関する限り、l.TryLock(または l.TryRLock)は、ミューテックス l がアンロックされている場合でも、false を返すことができると考えることができます。

一度実行

sync パッケージは、複数のゴルーチンが存在する場合の初期化のための安全なメカニズムを、Once 型の使用を通じて提供します。複数のスレッドが特定の f に対して once.Do(f) を実行できますが、f() を実行するのは1つだけで、他の呼び出しは 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 を呼び出すと、setup が正確に1回呼び出されます。setup 関数は、print のいずれかの呼び出しよりも前に完了します。結果として、"hello, world" が2回出力されます。

アトミック値

sync/atomic パッケージの API は、まとめて、異なるゴルーチンの実行を同期するために使用できる「アトミック操作」です。アトミック操作 A の効果がアトミック操作 B によって観測される場合、AB よりも前に同期されます。プログラムで実行されるすべてのアトミック操作は、逐次一貫性のある順序で実行されるかのように動作します。

前の定義は、C++ の逐次一貫性のあるアトミックと Java の volatile 変数と同じセマンティクスを持ちます。

ファイナライザー

runtime パッケージは、特定のオブジェクトがプログラムによってもはや到達可能でない場合に呼び出されるファイナライザーを追加する SetFinalizer 関数を提供します。SetFinalizer(x, f) の呼び出しは、ファイナライザー呼び出し f(x) よりも前に同期されます。

追加のメカニズム

sync パッケージは、条件変数、ロックフリーマップ、割り当てプール、待機グループなどの追加の同期抽象化を提供します。これらのそれぞれのドキュメントは、同期に関して保証することを規定します。

同期抽象化を提供する他のパッケージも、それらが保証することを文書化する必要があります。