线程安全的结构体设计
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 65 课,共 107 课。
现在你已经了解了互斥锁和等待组,让我们把它们结合起来,设计出可以由多个 goroutine 同时安全使用的结构体。线程安全的结构体会在其方法中封装同步操作,因此调用方不必担心加锁。
模式很简单:在你的结构体中嵌入一个互斥锁,并在每个访问共享状态的方法中加锁:
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() 方法也会锁定互斥锁。如果不这样做,一个 goroutine 可能会在另一个 goroutine 写入时读取,从而导致数据竞争。如果读取远多于写入,可以改用 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 都会以原子方式更新:没有任何 goroutine 能观察到一个已改变而另一个未改变的不一致状态。
挑战
简单让我们构建一个线程安全的银行账户系统,演示如何在结构体方法内部妥善封装同步机制。你的账户会安全地处理并发存款、取款和余额查询,而不会向调用者暴露任何锁的细节。
你会将代码分散到两个文件中:
account.go:定义你的线程安全银行账户。创建一个
BankAccount结构体,其中嵌入sync.Mutex,包含一个balance字段(int),以及一个transactions切片,用字符串记录所有成功的操作。实现以下方法:
NewBankAccount(initial int) *BankAccount:创建一个初始余额为给定值、且transactions切片为空的新账户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 fundsbalance:输出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(),完全不必考虑锁。你的结构体会在内部处理线程安全。
亲手试试
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: "
// 后续记录应各占一行,不加前缀
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器