Leapcell: The Best of Serverless Web Hosting
Go言語における sync
標準ライブラリパッケージの詳細説明
Go言語の並列プログラミングにおいて、sync
標準ライブラリパッケージは、並列同期を実装するための一連の型を提供しています。これらの型は、さまざまなメモリオーダリングの要件を満たすことができます。チャネルと比較して、特定のシナリオでこれらを使用すると、効率的であるだけでなく、コードの実装がより簡潔かつ明確になります。以下では、sync
パッケージ内のいくつかの一般的な型とその使用方法について詳細に説明します。
1. sync.WaitGroup
型(待機グループ)
sync.WaitGroup
は、goroutine間の同期を実現するために使用され、1つまたは複数のgoroutineが他のいくつかのgoroutineがタスクを完了するのを待つことができます。各 sync.WaitGroup
値は内部でカウントを保持しており、このカウントの初期デフォルト値はゼロです。
1.1 メソッドの説明
sync.WaitGroup
型には、3つのコアメソッドが含まれています:
-
Add(delta int)
:WaitGroup
が保持するカウントを変更するために使用されます。正の整数delta
が渡された場合、カウントは対応する値だけ増加します。負の数が渡された場合、カウントは対応する値だけ減少します。 -
Done()
:Add(-1)
と同等のショートカットであり、通常はgoroutineタスクが完了したときにカウントを1減らすために使用されます。 -
Wait()
:goroutineがこのメソッドを呼び出したとき、カウントがゼロの場合、この操作は何もしません(no operation)。カウントが正の整数の場合、現在のgoroutineはブロック状態に入り、カウントがゼロになるまで実行状態に戻らず、つまりWait()
メソッドは戻りません。
wg.Add(delta)
、wg.Done()
および wg.Wait()
はそれぞれ (&wg).Add(delta)
、(&wg).Done()
および (&wg).Wait()
の省略形であることに注意してください。Add(delta)
または Done()
の呼び出しによってカウントが負になった場合、プログラムはpanicします。
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)
}
上記の例では、メインgoroutineは wg.Add(N)
によって待機グループのカウントを5に設定し、その後5つのgoroutineを起動します。各goroutineはタスクを完了した後に wg.Done()
を呼び出してカウントを1減らします。メインgoroutineは wg.Wait()
を呼び出して、5つのすべてのgoroutineがタスクを完了してカウントが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
メソッドは複数のgoroutineで呼び出すことができます。対応する sync.WaitGroup
値が保持するカウントが0になったとき、これらのgoroutineはすべて通知を受け取り、ブロック状態を終了します。
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
型は、並列プログラム内で1つのコードがただ1回だけ実行されることを保証するために使用されます。各 *sync.Once
値には Do(f func())
メソッドがあり、これは func()
型のパラメータを受け取ります。
2.1 メソッドの特徴
アドレス指定可能な sync.Once
値 o
に対して、o.Do()
(すなわち (&o).Do()
の省略形)メソッドの呼び出しは、複数のgoroutineで同時実行されることができ、これらのメソッド呼び出しの引数は(義務ではありませんが)同じ関数値である必要があります。これらの呼び出しの中で、引数関数(値)のうちただ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つのgoroutineがすべて once.Do(doSomething)
を呼び出していますが、doSomething
関数はただ1回だけ実行されます。したがって、"Hello" はただ1回だけ出力され、"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
値がロックされると、新しいロック試行は現在のgoroutineをブロック状態に入れ、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
が複数のgoroutineによって同時にアクセスおよび変更されないようにしています。これにより、データの一貫性と正しさが保証されます。
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
の書き込みロックがロック状態にあるとき、新しい書き込みロックまたは読みみ取りロック操作は、現在のgoroutineをブロック状態に入れます。書き込みロックがロック解除されるまで続きます。 -
rwm
の読み取りロックがロック状態にあるとき、新しい書き込みロック操作は現在のgoroutineをブロック状態に入れます。そして、新しい読み取りロック操作は特定の条件下で(どのブロックされている書き込みロック操作よりも前に発生する場合)成功します。つまり、読み取りロックは複数のデータ読み取り専用者によって同時に保持することができます。読み取りロックが保持するカウントがゼロにクリアされると、読み取りロックはロック解除状態に戻ります。 - データ書き込み専用者が飢え状態にならないようにするために、読み取りロックがロック状態にあり、ブロックされている書き込みロック操作があるとき、後続の読み取りロック操作はブロックされます。データ読み取り専用者が飢え状態にならないようにするために、書き込みロックがロック状態にあるとき、書き込みロックがロック解除された後、以前にブロックされていた読み取りロック操作は必ず成功します。
-
- 使用例
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
で、これは読み取り-書き込みロックのロック規則を説明し、検証するために使用されます。プログラム内でgoroutine間の同期のために 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
を通じてgoroutine間で簡単な通知を実装しており、"Hi" が "Bye" の前に出力されることを保証しています。sync.Mutex
と sync.RWMutex
値に関連するメモリオーダリング保証については、Goにおけるメモリオーダリング保証の関連ドキュメントを参照することができます。
sync
標準ライブラリパッケージ内の型は、Go言語の並列プログラミングにおいて極めて重要な役割を果たします。開発者は特定のビジネスシナリオと要件に基づいて、これらの同期型を合理的に選択し、正しく使用する必要があります。これにより、効率的で信頼性の高いスレッドセーフな並列プログラムを書くことができます。同時に、並列コードを書くときには、並列プログラミングにおけるさまざまな概念や潜在的な問題(たとえば、データ競合、デッドロックなど)を深く理解する必要もあります。十分なテストと検証を通じて、並列環境におけるプログラムの正しさと安定性を保証する必要があります。
Leapcell: The Best of Serverless Web Hosting
最後に、Goサービスのデプロイに最適なプラットフォームをお勧めします:Leapcell
🚀 好きな言語で構築する
JavaScript、Python、Go、またはRustで簡単に開発できます。
🌍 無制限のプロジェクトを無料でデプロイする
使用する分だけ支払います —— リクエストがなければ請求されません。
⚡ 使った分だけ支払い、隠された費用はありません
アイドル料はかかりません。シームレスなスケーラビリティが備わっています。
🔹 Twitterでフォローしてください:@LeapcellHQ