sync.Mutex 与 sync.RWMutex
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 63 课,共 107 课。
虽然通道是 Go 中协调 goroutine 的首选方式,但有时你需要直接保护共享数据。sync 包提供了互斥锁:它可以确保同一时间只有一个 goroutine 访问某个资源。
sync.Mutex 有两个方法:Lock() 和 Unlock()。当一个 goroutine 调用 Lock() 时,它会获得独占访问权限。其他调用 Lock() 的 goroutine 会阻塞,直到调用 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() 可以确保即使函数提前返回或发生 panic,也会释放锁。这是一种常见且推荐的模式。
当读取操作频繁但写入操作很少时,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()。这样可以支持并发读取,同时确保写入安全。
挑战
简单让我们构建一个线程安全的库存系统,用来跟踪产品库存数量。你的系统会使用互斥锁安全地处理并发读写,确保多个操作同时发生时数据仍然完整。
你需要将代码分成两个文件:
inventory.go:定义线程安全的库存管理系统。创建一个
Inventory结构体,用映射存储产品数量,并使用sync.RWMutex保护访问。你的库存系统应支持以下操作: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 stockremove:成功时打印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 在修改库存时会获得独占访问权。
亲手试试
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:读取数量,减少库存,输出相应结果
}
}
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
9并发与面向对象编程
Goroutine 基础通道与通信缓冲通道与无缓冲通道select 语句sync.Mutex 与 sync.RWMutexsync.WaitGroup线程安全的结构体设计回顾:工作池自己练习: Go 在线编译器