Menu
Coddy logo textTech

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() を使用します。これにより、書き込みの安全性を確保しながら、並行読み取りが可能になります。

challenge icon

チャレンジ

簡単

製品の在庫レベルを追跡するスレッドセーフな在庫管理システムを構築しましょう。このシステムは、ミューテックスを使用して並行した読み取りと書き込みを安全に処理し、複数の操作が同時に発生した際にもデータの整合性を確保します。

コードは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: 操作を読み取り、スレッドセーフな在庫システムの動作を示します。

    操作の数を読み取り、各操作を処理します。各操作にはタイプ(addget、または remove)、製品名があり、add および remove 操作の場合は数量が含まれます。

    各操作について、結果を出力してください:

    • add: Added [quantity] [product] と出力します。
    • get: [product]: [stock] in stock と出力します。
    • remove: 成功した場合は Removed [quantity] [product]、失敗した場合は Insufficient stock for [product] と出力します。

以下の入力が提供されます:

  • 1行目:操作の数(整数)
  • 続く行:各操作について:
    • 操作タイプ(addget、または 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 stock

RWMutex を使用することで、複数の GetStock 呼び出しが同時に読み取りを行える一方で、AddStockRemoveStock は在庫を変更する際に排他的なアクセス権を取得します。

チートシート

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: 数量を読み込み、在庫を減らし、適切な結果を出力する
			
		}
	}
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン