スレッドセーフな構造体設計
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 65/107。
ミューテックスと WaitGroup について理解したところで、これらを組み合わせて、複数のゴルーチンから同時に安全に使用できる構造体を設計してみましょう。スレッドセーフな構造体は、そのメソッド内で同期をカプセル化するため、呼び出し側はロックについて心配する必要がありません。
パターンは単純です:構造体にミューテックスを埋め込み、共有状態にアクセスするすべてのメソッドでそれをロックします:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}読み取り専用の Value() メソッドでさえ、ミューテックスをロックしていることに注意してください。これがないと、あるゴルーチンが書き込みを行っている間に別のゴルーチンが読み取りを行う可能性があり、データ競合が発生します。もし書き込みよりも読み取りの方がはるかに多い場合は、代わりに sync.RWMutex を使用し、読み取りには RLock() を呼び出してください。
重要な設計原則:ミューテックスをプライベートに保つ。小文字のフィールド名(mu)を使用することで、外部コードが直接アクセスするのを防ぐことができます。すべての同期はメソッドを通じて行われるため、スレッドセーフを完全に制御できます。
複数のフィールドを持つ構造体では、一貫した状態を確保するために、関連するすべてのフィールドを同じミューテックスで保護します。
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}balance と history はどちらもアトミックに更新されます。どのゴルーチンも、一方が変更されてもう一方が変更されていないような不整合な状態を観測することはありません。
チャレンジ
簡単構造体メソッド内での同期の適切なカプセル化を実証する、スレッドセーフな銀行口座システムを構築しましょう。この口座は、呼び出し側にロックの詳細を公開することなく、並行した預け入れ、引き出し、および残高確認を安全に処理します。
コードは2つのファイルに分けて構成します:
account.go: スレッドセーフな銀行口座を定義します。sync.Mutexを埋め込み、balanceフィールド(int)、およびすべての成功した操作を文字列として記録するtransactionsスライスを持つBankAccount構造体を作成します。以下のメソッドを実装してください:
NewBankAccount(initial int) *BankAccount- 指定された初期残高と空のトランザクションスライスを持つ新しい口座を作成しますDeposit(amount int)- 残高に金額を追加し、トランザクションを+[amount]として記録しますWithdraw(amount int) bool- 十分な資金がある場合、金額を差し引き、-[amount]を記録してtrueを返します。それ以外の場合は、何も変更せずにfalseを返しますBalance() int- 現在の残高を返しますHistory() []string- トランザクションスライスのコピーを返します
構造体のフィールドにアクセスするすべてのメソッドは、スレッドセーフを確保するためにミューテックスをロックする必要があります。アンロックには
deferを使用してください。外部コードがメソッドを使用するように、ミューテックスとすべてのフィールドは非公開(小文字)のままにしてください。main.go: 銀行業務を処理し、スレッドセーフな口座を実証します。初期残高、次に操作回数を読み取ります。各操作について、タイプ(
deposit、withdraw、またはbalance)を読み取り、預け入れ/引き出しの場合は金額を読み取ります。各操作の結果を出力します:
deposit:Deposited [amount], Balance: [new balance]と出力しますwithdraw: 成功した場合はWithdrew [amount], Balance: [new balance]、失敗した場合はWithdrawal failed: insufficient fundsと出力しますbalance:Current balance: [balance]と出力します
すべての操作の後、トランザクション履歴を各エントリを新しい行に出力し、最初のエントリにのみ
History:をプレフィックスとして付けます。
以下の入力が提供されます:
- 1行目:初期残高(整数)
- 2行目:操作回数(整数)
- 続く行:各操作について、タイプ(
deposit、withdraw、またはbalance)、および預け入れ/引き出しの場合は次の行に金額
例えば、以下のように与えられた場合:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balance出力は以下のようになります:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30ここでの重要な原則は、すべての同期が BankAccount メソッドの内部に隠されていることです。呼び出し側はロックについて考えることなく、単に Deposit()、Withdraw()、および Balance() を使用します。構造体が内部でスレッドセーフを処理します。
チートシート
スレッドセーフな構造体は、ミューテックスを埋め込み、共有状態にアクセスするすべてのメソッドでそれをロックすることで、メソッド内での同期をカプセル化します。
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}主な原則:
- データレースを防ぐため、読み取り専用メソッドを含む、共有状態にアクセスするすべてのメソッドでミューテックスをロックします
- 関数が早期にリターンした場合でもミューテックスが確実にアンロックされるように、
deferを使用します - 外部のコードが直接アクセスできないように、ミューテックスをプライベート(小文字のフィールド名)にします
- 読み取りが多いワークロードの場合は、
sync.RWMutexを使用し、読み取りにはRLock()を呼び出します
複数のフィールドを持つ構造体の場合、一貫した状態を保証するために、関連するすべてのフィールドを同じミューテックスで保護します:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}これにより、balanceとhistoryの両方がアトミックに更新されることが保証され、どのゴルーチンも不整合な状態を観測することはありません。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 初期の残高を読み込む
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// 操作の回数を読み込む
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// 銀行口座を作成する
account := NewBankAccount(initial)
// 各操作を処理する
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Depositを呼び出し、結果を出力する
// 形式: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Withdrawを呼び出し、適切な結果を出力する
// 成功時: "Withdrew [amount], Balance: [new balance]"
// 失敗時: "Withdrawal failed: insufficient funds"
_ = amount // 実装時にこの行を削除してください
case "balance":
// TODO: Balanceを呼び出し、結果を出力する
// 形式: "Current balance: [balance]"
}
}
// TODO: 取引履歴を出力する
// 最初の項目には "History: " という接頭辞を付けること
// それ以降の項目は接頭辞なしで新しい行に出力すること
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー