Menu
CoddyTech

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()。这样可以支持并发读取,同时确保写入安全。

challenge icon

挑战

简单

让我们构建一个线程安全的库存系统,用来跟踪产品库存数量。你的系统会使用互斥锁安全地处理并发读写,确保多个操作同时发生时数据仍然完整。

你需要将代码分成两个文件:

  • 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 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 stock

RWMutex 允许多个 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:读取数量,减少库存,输出相应结果
			
		}
	}
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器