ミューテックス(Mutex) - Golang learning step 2-8
- 公開日
- カテゴリ:GoingDeeper
- タグ:Golang,roadmap.sh,学習メモ
roadmap.sh > Go > Going Deeper > Mutex の学習を進めていきます。
※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。
contents
開発環境
- チップ: Apple M2 Pro
- OS: macOS Sonoma
- go version: go1.23.2 darwin/arm64
参考 URL
ミューテックス(Mutex)
Go ではゴルーチンを使用することで、コードを並行して実行することができます。しかし、並行処理が同じデータにアクセスする場合、競合状態(レースコンディション)が発生する可能性があります。ミューテックスは、sync パッケージによって提供されるデータ構造です。データの特定の部分にロックをかけることができ、一度に 1 つのゴルーチンだけがそのデータにアクセスできるようになります。
GoのMutex(ミューテックス)は、ゴルーチン間での排他制御を実現するためのデータ構造です。これは、同時に複数のゴルーチンが同じデータにアクセスすることによるデータ競合を防ぐために使用されます。
Go の Mutex は、並行処理においてデータ競合を防ぐために使われるロック機構です。複数のゴルーチンが同時に同じリソースにアクセスしようとする場合、データの不整合が発生する可能性があるため、Mutex を利用してデータへのアクセスを制御します。
基本的な使い方
Mutex は sync パッケージに含まれており、主に以下の2つのメソッドを使用します。
Lock()
: Mutex をロックし、他のゴルーチンが同じリソースにアクセスできないようにします。Unlock()
: Mutex を解除し、他のゴルーチンがリソースにアクセスできるようにします。
1. インポート
syncパッケージをインポートします。
import "sync"
2. Mutexの定義
sync.Mutex
型の変数を定義します
var mu sync.Mutex
3. ロックとアンロック
共有データにアクセスする際は、Lock()
メソッドでロックを取得し、操作が終わったら Unlock()
メソッドでロックを解放します。
mu.Lock() // ロックを取得
// 共有データに対する操作
mu.Unlock() // ロックを解放
使用例
以下は、Go の Mutex を使った簡単な例です。この例では、複数のゴルーチンがカウンタを安全に更新します。
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done() // ゴルーチン終了時にWaitGroupのカウントを減らす
mu.Lock() // ロックを取得
counter++ // カウンタをインクリメント
mu.Unlock() // ロックを解放
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1) // ゴルーチンの数を追加
go increment(&wg)
}
wg.Wait() // 全てのゴルーチンが終了するのを待つ
fmt.Println("Final counter:", counter)
}
// 出力: Final counter: 10
この例では、counter 変数へのアクセスを Mutex で制御し、データ競合を防いでいます。Lock メソッドでロックしている間は他のゴルーチンが counter にアクセスできず、Unlock メソッドでロック解除後に次のゴルーチンがアクセス可能になります。
Mutex の注意点
- デッドロック
- Mutex の使用中にロックを獲得できない場合、デッドロックが発生する可能性があります。ロックを獲得したら、必ず
Unlock()
を呼び出してロックを解放することが重要。
- Mutex の使用中にロックを獲得できない場合、デッドロックが発生する可能性があります。ロックを獲得したら、必ず
- 必要以上のロックを避ける
- ロック範囲を最小限に抑えることで、パフォーマンスが向上する。
- コピーの禁止
- Mutex はコピーして使用してはいけない
- ポインタとして渡す必要がある
ベストプラクティス
1. スコープを最小限に
func ProcessData(data []int) {
// Mutex でロックが必要ない処理
result := heavyComputation(data)
// 必要な箇所だけをロック
mu.Lock()
updateSharedResource(result)
mu.Unlock()
}
2. 構造化された使用
Mutex の構造化された使用とは、以下の設計原則に基づくアプローチです。
- カプセル化
- Mutex とそれが保護するデータを同じ構造体にまとめる
- データへのアクセスをメソッドを通じてのみ行うようにする
- 責務の明確化
- 構造体が自身のデータの同期を責務として持つ
- 利用者側は同期のことを意識する必要がない
悪い例:構造化されていない使用
// 悪い例:グローバルな変数と Mutex
var (
mu sync.Mutex
counts map[string]int
)
// 問題点:
// 1. グローバル変数は危険
// 2. counts と mu の関係が暗黙的
// 3. 他の場所から counts に直接アクセスできてしまう
func IncrementCount(key string) {
mu.Lock()
defer mu.Unlock()
counts[key]++
}
良い例:構造化された使用
package main
import (
"fmt"
"sync"
)
// 良い例:カプセル化された構造体
type SafeCounter struct {
mu sync.Mutex
counts map[string]int
}
// NewSafeCounter はカウンターを適切に初期化する
func NewSafeCounter() *SafeCounter {
return &SafeCounter{
counts: make(map[string]int),
}
}
// Increment は安全にカウントを増やす
func (sc *SafeCounter) Increment(key string) {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.counts[key]++
}
// GetCount は安全にカウントを取得する
func (sc *SafeCounter) GetCount(key string) int {
sc.mu.Lock()
defer sc.mu.Unlock()
return sc.counts[key]
}
func main() {
counter := NewSafeCounter()
var wg sync.WaitGroup
wg.Add(2)
// 複数のゴルーチンから安全に使用可能
go func() {
defer wg.Done()
counter.Increment("x")
}()
go func() {
defer wg.Done()
counter.Increment("x")
}()
wg.Wait()
count := counter.GetCount("x")
fmt.Printf("x: %d\n", count)
}
// 出力: x: 2
3. 適切な粒度の選択
- 細かすぎるロックは複雑性を増加
- 粗すぎるロックはパフォーマンスを低下
- ユースケースに応じて適切な粒度を選択
Go の Mutex は、共有データに対する安全なアクセスを提供し、データ競合を防ぐために欠かせない機能です。適切に使用することで、効率的で安全な並行処理を実現できます。
RWMutex
sync.RWMutex
は Go の sync パッケージに含まれている排他制御の一種で、読み取り専用のアクセスと書き込みアクセスの両方を効率的に制御するためのミューテックスです。
通常の Mutex は読み取りや書き込みに関係なくロックをかけますが、RWMutex は読み取りアクセスに対しては複数のゴルーチンが同時にアクセスでき、書き込みアクセスに対しては単独でロックをかけることで、より柔軟で効率的な排他制御が可能です。
基本的な使い方
- 読み取りロック (
RLock()
)- 複数のゴルーチンが同時にデータを読み取れるようにロックを取得します。読み取りロックは他の読み取りロックと共存できますが、書き込みロックと同時には取得できません。
- 書き込みロック (
Lock()
)- データの更新や書き込みを行うときにロックを取得します。書き込みロックは読み取り・書き込みロックのいずれとも同時に取得することができず、完全に排他的です。
- 読み取りロックの解放 (
RUnlock()
)- 読み取りロックを解放します。
- 書き込みロックの解放 (
Unlock()
)- 書き込みロックを解放します。
RWMutex の使用例
以下のコードは、RWMutex を使って複数のゴルーチンがデータを安全に読み書きする例です。読み取りは同時に複数のゴルーチンから行えますが、書き込みは排他制御されます。
package main
import (
"fmt"
"sync"
)
var (
data int
rwMutex sync.RWMutex
)
func read(wg *sync.WaitGroup, id int) {
defer wg.Done()
rwMutex.RLock() // 読み取りロックを取得
fmt.Printf("Reader %d: Data is %d\n", id, data)
rwMutex.RUnlock() // 読み取りロックを解放
}
func write(wg *sync.WaitGroup, value int) {
defer wg.Done()
rwMutex.Lock() // 書き込みロックを取得
data = value
fmt.Printf("Writer: Wrote data %d\n", data)
rwMutex.Unlock() // 書き込みロックを解放
}
func main() {
var wg sync.WaitGroup
// 複数の読み取りゴルーチンを起動
for i := 1; i <= 3; i++ {
wg.Add(1)
go read(&wg, i)
}
// 書き込みゴルーチンを起動
wg.Add(1)
go write(&wg, 42)
// 再び読み取りゴルーチンを起動
for i := 4; i <= 6; i++ {
wg.Add(1)
go read(&wg, i)
}
wg.Wait()
}
この例では、読み取りゴルーチンが同時にデータを読み取り、書き込みゴルーチンは単独でデータを更新します。
Reader 2: Data is 0
Reader 6: Data is 0
Writer: Wrote data 42
Reader 3: Data is 42
Reader 4: Data is 42
Reader 1: Data is 42
Reader 5: Data is 42
RWMutex の利点
- 効率的な読み取り
- データの読み取りが多数のゴルーチンから行われる場合、RWMutex により読み取りの効率が向上します。読み取りは同時に複数のゴルーチンから安全に実行できるため、システムのパフォーマンスを向上させます。
- 書き込みの排他制御
- 書き込みが発生する際にはすべてのアクセスが排他制御され、データの整合性を確保します。
注意点
- デッドロック
- 通常の Mutex と同様、RWMutex もデッドロックを防ぐために、ロックを取得したら必ず解放する必要がある。
- 使い過ぎによる性能低下
- RWMutex は読み取りが多く書き込みが少ない場合に有効だが、書き込みが多い場合には単純な Mutex の方がパフォーマンスが良くなることもある。
RWMutex は、特に読み取りが頻繁で書き込みが少ない場合に適した排他制御の手段です。適切に使用することで、並行処理における効率的なデータアクセスを実現できます。
まとめ
- Go では並行処理でデータ競合を防ぐために Mutex を使用
- Mutex はデータへのアクセスをロックし、複数のゴルーチンからの同時アクセスを制限
sync
パッケージで提供されるLock()
とUnlock()
メソッドを活用- RWMutex を使用することで、読み取り専用アクセスを効率化
- デッドロックや性能低下を防ぐため、ロックのスコープと使用頻度に注意
[Next] Step 2-9: スケジューラー(Scheduler)
[Prev] Step 2-7: Select 構文