sync.Mutex と sync.RWMutex
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 63/107。
チャネルはゴルーチンを調整するためのGoの推奨される方法ですが、時には共有データを直接保護する必要がある場合もあります。syncパッケージはミューテックスを提供します。これは、一度に1つのゴルーチンだけがリソースにアクセスすることを保証するロックです。
sync.Mutex には、Lock() と Unlock() の2つのメソッドがあります。ゴルーチンが Lock() を呼び出すと、排他的なアクセス権を取得します。Lock() を呼び出す他のゴルーチンは、Unlock() が呼び出されるまでブロックされます:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}defer c.mu.Unlock() を使用することで、関数が早期にリターンしたりパニックが発生したりした場合でも、ロックが確実に解放されるようになります。これは一般的で推奨されるパターンです。
読み込みが頻繁で書き込みが稀な場合、sync.RWMutex はより優れたパフォーマンスを提供します。これは複数の読み取りを同時に許可しますが、書き込みには排他的なアクセス権が与えられます:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // 複数の読み取りを許可
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // 書き込みのための排他的アクセス
defer c.mu.Unlock()
c.data[key] = value
}読み取り操作には RLock()/RUnlock() を使用し、書き込みには Lock()/Unlock() を使用します。これにより、書き込みの安全性を確保しながら、並行読み取りが可能になります。
チャレンジ
簡単製品の在庫レベルを追跡するスレッドセーフな在庫管理システムを構築しましょう。このシステムは、ミューテックスを使用して並行した読み取りと書き込みを安全に処理し、複数の操作が同時に発生した際にもデータの整合性を確保します。
コードは2つのファイルに分けて構成します:
inventory.go: スレッドセーフな在庫管理システムを定義します。マップに製品の数量を保存し、アクセスを保護するために
sync.RWMutexを使用するInventory構造体を作成します。在庫システムは以下の操作をサポートする必要があります:NewInventory() *Inventory- 初期化されたマップを持つ新しい在庫インスタンスを作成します。AddStock(product string, quantity int)- 製品の在庫に数量を追加します(データを変更するため、排他ロックを使用してください)。GetStock(product string) int- 製品の現在の在庫を返します。見つからない場合は 0 を返します(データの読み取りのみであるため、読み取りロックを使用してください)。RemoveStock(product string, quantity int) bool- 十分な在庫がある場合、在庫から数量を減らします。成功した場合はtrue、在庫不足の場合はfalseを返します(排他ロックを使用してください)。
ロックが常に適切に解放されるよう、アンロックには
deferを使用することを忘れないでください。main.go: 操作を読み取り、スレッドセーフな在庫システムの動作を示します。操作の数を読み取り、各操作を処理します。各操作にはタイプ(
add、get、またはremove)、製品名があり、addおよびremove操作の場合は数量が含まれます。各操作について、結果を出力してください:
add:Added [quantity] [product]と出力します。get:[product]: [stock] in stockと出力します。remove: 成功した場合はRemoved [quantity] [product]、失敗した場合はInsufficient stock for [product]と出力します。
以下の入力が提供されます:
- 1行目:操作の数(整数)
- 続く行:各操作について:
- 操作タイプ(
add、get、またはremove) - 製品名
- 数量(
addおよびremove操作のみ)
- 操作タイプ(
例えば、以下のような入力の場合:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
apples出力は以下のようになります:
Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stockRWMutex を使用することで、複数の GetStock 呼び出しが同時に読み取りを行える一方で、AddStock と RemoveStock は在庫を変更する際に排他的なアクセス権を取得します。
チートシート
sync パッケージは、複数のゴルーチンが同じリソースに直接アクセスする必要がある場合に、共有データを保護するための ミューテックス (mutexes) を提供します。
sync.Mutex
sync.Mutex は、Lock() と Unlock() を使用して排他的なアクセスを提供します。
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}Unlock() と共に defer を使用することで、関数が早期にリターンしたりパニックが発生したりした場合でも、確実にロックが解除されるようになります。
sync.RWMutex
sync.RWMutex は、複数の同時実行リーダーを許可しますが、ライターに対しては排他的なアクセスを提供します。
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // multiple readers allowed
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // exclusive access for writing
defer c.mu.Unlock()
c.data[key] = value
}読み取り操作には RLock()/RUnlock() を使用し、書き込み操作には Lock()/Unlock() を使用します。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 操作の数を読み込む
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// 新しい在庫を作成する
inventory := NewInventory()
// 各操作を処理する
for i := 0; i < numOps; i++ {
// 操作タイプを読み込む
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// 商品名を読み込む
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: 各操作タイプ(add、get、remove)を処理する
// "add" と "remove" の場合、入力から数量を読み込む
// 適切な在庫メソッドを呼び出す
// チャレンジの説明に従って結果を出力する
switch opType {
case "add":
// TODO: 数量を読み込み、在庫を追加し、結果を出力する
case "get":
// TODO: 在庫を取得し、結果を出力する
case "remove":
// TODO: 数量を読み込み、在庫を減らし、適切な結果を出力する
}
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー9並行処理とオブジェクト指向
Goroutine の基本Channel と通信バッファあり Channel とバッファなし Channelselect 文sync.Mutex と sync.RWMutexsync.WaitGroupスレッドセーフな構造体設計まとめ - Worker Pool