Goのsyncパッケージ:並行処理同期テクニック集
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go言語のsyncパッケージ:並行処理同期テクニック集
Go言語の並行プログラミングにおいて、sync
標準ライブラリパッケージは一連の並行処理同期を実装するための型を提供します。これらの型は、さまざまなメモリ順序付けの要件を満たすことができます。チャネルと比較して、特定のシナリオで使用すると、より効率的であるだけでなく、コードの実装がより簡潔で明確になります。以下では、sync
パッケージで一般的に使用されるいくつかの型とその使用方法について詳しく紹介します。
1. sync.WaitGroup
型(ウェイトグループ)
sync.WaitGroup
は、ゴルーチン間の同期を実現するために使用され、1つまたは複数のゴルーチンが他のいくつかのゴルーチンのタスクが完了するのを待つことを可能にします。各sync.WaitGroup
の値は内部的にカウントを保持し、このカウントの初期デフォルト値はゼロです。
1.1 メソッドの紹介
sync.WaitGroup
型には、3つのコアメソッドが含まれています。
Add(delta int)
:WaitGroup
によって保持されるカウントを変更するために使用されます。正の整数delta
が渡されると、カウントは対応する値だけ増加します。負の数が渡されると、カウントは対応する値だけ減少します。Done()
:Add(-1)
の同等のショートカットであり、通常、ゴルーチンタスクが完了したときにカウントを1減らすために使用されます。Wait()
: ゴルーチンがこのメソッドを呼び出すとき、カウントがゼロの場合、この操作はno-op(何もしない)です。カウントが正の整数の場合、現在のゴルーチンはブロックされた状態になり、カウントがゼロになるまで実行状態に戻りません。つまり、Wait()
メソッドが返ります。
wg.Add(delta)
、wg.Done()
、およびwg.Wait()
は、それぞれ(&wg).Add(delta)
、(&wg).Done()
、および(&wg).Wait()
の省略形であることに注意してください。Add(delta)
またはDone()
の呼び出しによってカウントが負になると、プログラムはパニックになります。
1.2 使用例
package main import ( "fmt" "math/rand" "sync" "time" ) func main() { rand.Seed(time.Now().UnixNano()) // Go 1.20より前は必須 const N = 5 var values [N]int32 var wg sync.WaitGroup wg.Add(N) for i := 0; i < N; i++ { i := i go func() { values[i] = 50 + rand.Int31n(50) fmt.Println("Done:", i) wg.Done() // <=> wg.Add(-1) }() } wg.Wait() // すべての要素が初期化されていることが保証されます。 fmt.Println("values:", values) }
上記の例では、メインゴルーチンはwg.Add(N)
を通じてウェイトグループのカウントを5に設定し、その後、5つのゴルーチンを開始します。各ゴルーチンは、タスクが完了した後、wg.Done()
を呼び出してカウントを1減らします。メインゴルーチンはwg.Wait()
を呼び出して、5つのゴルーチンすべてがタスクを完了し、カウントが0になるまでブロックします。その後、後続のコードを実行して、各要素の値を出力します。
さらに、Add
メソッドの呼び出しは、以下に示すように、複数回に分割することもできます。
... var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) // 5回実行されます i := i go func() { values[i] = 50 + rand.Int31n(50) wg.Done() }() } ...
*sync.WaitGroup
値のWait
メソッドは、複数のゴルーチンで呼び出すことができます。対応するsync.WaitGroup
値によって保持されるカウントが0になると、これらのゴルーチンはすべて通知を受け取り、ブロックされた状態を終了します。
func main() { rand.Seed(time.Now().UnixNano()) // Go 1.20より前は必須 const N = 5 var values [N]int32 var wgA, wgB sync.WaitGroup wgA.Add(N) wgB.Add(1) for i := 0; i < N; i++ { i := i go func() { wgB.Wait() // ブロードキャスト通知を待つ log.Printf("values[%v]=%v \n", i, values[i]) wgA.Done() }() } // 次のループは、上記のいずれよりも前に実行されることが保証されています // wg.Wait 呼び出しが終了します。 for i := 0; i < N; i++ { values[i] = 50 + rand.Int31n(50) } wgB.Done() // ブロードキャスト通知を送信 wgA.Wait() }
WaitGroup
は、Wait
メソッドが返った後に再利用できます。ただし、WaitGroup
値によって保持されるベース番号がゼロの場合、正の整数の引数を持つAdd
メソッドの呼び出しは、Wait
メソッドの呼び出しと同時に実行することはできません。そうしないと、データ競合の問題が発生する可能性があります。
2. sync.Once
型
sync.Once
型は、並行プログラムでコードが一度だけ実行されるようにするために使用されます。各*sync.Once
値にはDo(f func())
メソッドがあり、func()
型のパラメータを受け入れます。
2.1 メソッドの特性
アドレス指定可能なsync.Once
値o
の場合、o.Do()
(つまり、(&o).Do()
の省略形)メソッドの呼び出しは、複数のゴルーチンで同時に複数回実行でき、これらのメソッド呼び出しの引数は、(必須ではありませんが)同じ関数値である必要があります。これらの呼び出しのうち、引数関数(値)の1つだけが呼び出され、呼び出された引数関数は、o.Do()
メソッド呼び出しが返る前に終了することが保証されています。つまり、呼び出された引数関数内のコードは、o.Do()
メソッドが呼び出しを返す前に実行されます。
2.2 使用例
package main import ( "log" "sync" ) func main() { log.SetFlags(0) x := 0 doSomething := func() { x++ log.Println("Hello") } var wg sync.WaitGroup var once sync.Once for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() once.Do(doSomething) log.Println("world!") }() } wg.Wait() log.Println("x =", x) // x = 1 }
上記の例では、5つのゴルーチンがすべてonce.Do(doSomething)
を呼び出していますが、doSomething
関数は一度だけ実行されます。したがって、「Hello」は一度だけ出力され、「world!」は5回出力され、「Hello」は必ず5つの「world!」出力の前に出力されます。
3. sync.Mutex
(ミューテックスロック)およびsync.RWMutex
(読み取り/書き込みロック)型
*sync.Mutex
型と*sync.RWMutex
型の両方が、sync.Locker
インターフェース型を実装します。したがって、これらの2つの型は両方ともLock()
メソッドとUnlock()
メソッドを含み、これらはデータを保護し、複数のユーザーが同時に読み取りおよび変更することを防ぐために使用されます。
3.1 sync.Mutex
(ミューテックスロック)
- 基本的な特性:
Mutex
のゼロ値は、ロック解除されたミューテックスです。アドレス指定可能なMutex
値m
の場合、ロック解除状態にある場合にのみ、m.Lock()
メソッドを呼び出すことで正常にロックできます。m
値がロックされると、新しいロック試行により、m.Unlock()
メソッドを呼び出してロック解除されるまで、現在のゴルーチンがブロックされた状態になります。m.Lock()
とm.Unlock()
は、それぞれ(&m).Lock()
と(&m).Unlock()
の省略形です。 - 使用例
package main import ( "fmt" "runtime" "sync" ) type Counter struct { m sync.Mutex n uint64 } func (c *Counter) Value() uint64 { c.m.Lock() defer c.m.Unlock() return c.n } func (c *Counter) Increase(delta uint64) { c.m.Lock() c.n += delta c.m.Unlock() } func main() { var c Counter for i := 0; i < 100; i++ { go func() { for k := 0; k < 100; k++ { c.Increase(1) } }() } // このループはデモンストレーションのみを目的としています。 for c.Value() < 10000 { runtime.Gosched() } fmt.Println(c.Value()) // 10000 }
上記の例では、Counter
構造体はMutex
フィールドm
を使用して、フィールドn
が複数のゴルーチンによって同時にアクセスおよび変更されないようにし、データの整合性と正確性を確保します。
3.2 sync.RWMutex
(読み取り/書き込みミューテックスロック)
- 基本的な特性:
sync.RWMutex
は内部的に、書き込みロックと読み取りロックの2つのロックを含んでいます。Lock()
メソッドとUnlock()
メソッドに加えて、*sync.RWMutex
型にはRLock()
メソッドとRUnlock()
メソッドもあり、これらは複数のリーダーが同時にデータを読み取ることをサポートするために使用されますが、データがライターと他のデータアクセッサー(リーダーとライターを含む)によって同時に使用されることを防ぎます。rwm
の読み取りロックはカウントを保持します。rwm.RLock()
呼び出しが成功すると、カウントは1増加します。rwm.RUnlock()
呼び出しが成功すると、カウントは1減少します。カウントがゼロの場合、読み取りロックがロック解除状態にあることを示し、ゼロ以外のカウントは、読み取りロックがロックされていることを示します。rwm.Lock()
、rwm.Unlock()
、rwm.RLock()
、およびrwm.RUnlock()
は、それぞれ(&rwm).Lock()
、(&rwm).Unlock()
、(&rwm).RLock()
、および(&rwm).RUnlock()
の省略形です。 - ロックのルール
rwm
の書き込みロックは、書き込みロックと読み取りロックの両方がロック解除状態にある場合にのみ正常にロックできます。つまり、書き込みロックは、いつでも最大1つのデータライターによって正常にロックでき、書き込みロックと読み取りロックを同時にロックすることはできません。rwm
の書き込みロックがロック状態にある場合、新しい書き込みロックまたは読み取りロック操作により、現在のゴルーチンは書き込みロックがロック解除されるまでブロックされた状態になります。rwm
の読み取りロックがロック状態にある場合、新しい書き込みロック操作により、現在のゴルーチンはブロックされた状態になります。新しい読み取りロック操作は、特定の条件下で(ブロックされた書き込みロック操作が発生する前に)成功します。つまり、読み取りロックは複数のデータリーダーによって同時に保持できます。読み取りロックによって保持されるカウントがゼロにクリアされると、読み取りロックはロック解除状態に戻ります。- データライターの飢餓を防ぐために、読み取りロックがロック状態にあり、ブロックされた書き込みロック操作がある場合、後続の読み取りロック操作はブロックされます。データリーダーの飢餓を防ぐために、書き込みロックがロック状態にある場合、書き込みロックがロック解除された後、以前にブロックされた読み取りロック操作は必ず成功します。
- 使用例
package main import ( "fmt" "time" "sync" ) func main() { var m sync.RWMutex go func() { m.RLock() fmt.Print("a") time.Sleep(time.Second) m.RUnlock() }() go func() { time.Sleep(time.Second * 1 / 4) m.Lock() fmt.Print("b") time.Sleep(time.Second) m.Unlock() }() go func() { time.Sleep(time.Second * 2 / 4) m.Lock() fmt.Print("c") m.Unlock() }() go func () { time.Sleep(time.Second * 3 / 4) m.RLock() fmt.Print("d") m.RUnlock() }() time.Sleep(time.Second * 3) fmt.Println() }
上記のプログラムは、abdc
を出力する可能性が最も高く、これは読み取り/書き込みロックのロックルールを説明および検証するために使用されます。プログラムでのゴルーチン間の同期のためのtime.Sleep
呼び出しの使用は、本番コードで使用しないでください。
実際には、読み取り操作が頻繁で書き込み操作が少ない場合、Mutex
をRWMutex
に置き換えて、実行効率を向上させることができます。たとえば、上記のCounter
の例のMutex
をRWMutex
に置き換えます。
... type Counter struct { //m sync.Mutex m sync.RWMutex n uint64 } func (c *Counter) Value() uint64 { //c.m.Lock() //defer c.m.Unlock() c.m.RLock() defer c.m.RUnlock() return c.n } ...
さらに、sync.Mutex
およびsync.RWMutex
の値を使用して通知を実装することもできますが、これはGoで最もエレガントな実装ではありません。例:
package main import ( "fmt" "sync" "time" ) func main() { var m sync.Mutex m.Lock() go func() { time.Sleep(time.Second) fmt.Println("Hi") m.Unlock() // 通知を送信 }() m.Lock() // 通知を待つ fmt.Println("Bye") }
この例では、Mutex
を介してゴルーチン間の単純な通知が実装され、「Hi」が「Bye」の前に出力されるようにします。sync.Mutex
およびsync.RWMutex
値に関連するメモリ順序付けの保証については、Goのメモリ順序付けの保証に関する関連ドキュメントを参照してください。
sync
標準ライブラリパッケージの型は、Go言語の並行プログラミングにおいて重要な役割を果たします。開発者は、特定のビジネスシナリオと要件に従って、これらの同期型を合理的に選択して正しく使用し、効率的で信頼性が高く、スレッドセーフな並行プログラムを作成する必要があります。同時に、並行コードを記述する際には、データ競合、デッドロックなど、並行プログラミングにおけるさまざまな概念と潜在的な問題について深く理解し、十分なテストと検証を通じて、並行環境でのプログラムの正確性と安定性を確保する必要があります。
Leapcell:最高のサーバーレスWebホスティング
最後に、Goサービスのデプロイに最適なプラットフォームをお勧めします:Leapcell
🚀 お気に入りの言語で構築する
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無制限のプロジェクトを無料でデプロイする
使用量に応じてのみ課金されます—リクエストも料金もありません。
⚡ 従量課金制、隠れたコストなし
アイドル料金はなく、シームレスなスケーラビリティだけです。
🔹 Twitterでフォローしてください:@LeapcellHQ