Goのsyncパッケージによるスレッドセーフな同期処理入門


[![Group195.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/82f526de-bb86-4421-b2de-e61e24a7af1c.png)](https://leapcell.io/)



**[Leapcell: The Best of Serverless Web Hosting](https://leapcell.io/)**

# 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 使用例
```go
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` メソッドの呼び出しは複数回に分割することもできます。以下に示す通りです：
```go
...
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はすべて通知を受け取り、ブロック状態を終了します。
```go
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 使用例
```go
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()` の省略形です。
- **使用例**
```go
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をブロック状態に入れます。そして、新しい読み取りロック操作は特定の条件下で（どのブロックされている書き込みロック操作よりも前に発生する場合）成功します。つまり、読み取りロックは複数のデータ読み取り専用者によって同時に保持することができます。読み取りロックが保持するカウントがゼロにクリアされると、読み取りロックはロック解除状態に戻ります。
    - データ書き込み専用者が飢え状態にならないようにするために、読み取りロックがロック状態にあり、ブロックされている書き込みロック操作があるとき、後続の読み取りロック操作はブロックされます。データ読み取り専用者が飢え状態にならないようにするために、書き込みロックがロック状態にあるとき、書き込みロックがロック解除された後、以前にブロックされていた読み取りロック操作は必ず成功します。
- **使用例**
```go
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` に置き換えます：
```go
...
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における最もエレガントな実装ではありません。たとえば：
```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](https://leapcell.io/)
最後に、Goサービスのデプロイに最適なプラットフォームをお勧めします：**[Leapcell](https://leapcell.io/)**

[![brandpic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/e6a3cc62-47ed-47ac-9420-cf8479cb4774.png)](https://leapcell.io/)


### 🚀 好きな言語で構築する
JavaScript、Python、Go、またはRustで簡単に開発できます。  

### 🌍 無制限のプロジェクトを無料でデプロイする
使用する分だけ支払います —— リクエストがなければ請求されません。  

### ⚡ 使った分だけ支払い、隠された費用はありません
アイドル料はかかりません。シームレスなスケーラビリティが備わっています。

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/85c6d3fe-d08e-4588-83ca-cdc8685b37f2.png)](https://leapcell.io/)


[📖 ドキュメントを参照する](https://docs.leapcell.io/)  

🔹 Twitterでフォローしてください：[@LeapcellHQ](https://x.com/LeapcellHQ)