~/posts/go-lock-bootcamp

Go 锁机制情景训练营:12 关,从 goroutine 打到 CAS

学并发最怕「看懂了」:读完 sync.Mutex 的文档觉得很简单,一写就出数据竞争。golangexe 换了个练法:先预测,再运行;先自己改,再要提示。仓库里没有参考答案。

玩法#

每一关是一个目录,里面有 README.md(场景和任务)、main.go(有问题的代码)和 main_test.go(验收测试):

  1. 按顺序进一关,读 README;
  2. 运行前先写下预测:结果对不对?每次都一样吗?
  3. 从项目根目录运行;
  4. 只改 main.go,不许改测试来绕过要求;
  5. 普通测试和竞态检测都要通过。
BASH
go run ./level01
go run -race ./level01
go test -v ./level01
go test -race -v ./level01

求助也分了三档:「给我第 N 关一个提示」只给方向;「讲讲第 N 关的思路」解释机制但不给代码;「解答第 N 关」才给完整方案。拿去配合 AI 助手用正合适:想让它帮忙,又不想被剧透。

关卡地图#

关卡场景知识点
01忙碌的咖啡店goroutine 与调度
02搬运队协作WaitGroup 与 channel
03游戏积分榜数据竞争与 Mutex
04公共打印机defer 解锁与锁范围
05银行账户保护数据不变量
06热门文章服务RWMutex
07账户互转死锁与锁顺序
08仓库取货sync.Cond
09配置中心启动sync.Once
10高并发计数服务锁竞争与分片锁
11限量商品抢购atomic 与 CAS
12演唱会抢票系统综合实战

注意第 06 和第 10 关:starter 代码的测试一开始就是绿的。06 关还要你人工观察读多写少的场景并改进,10 关要实现优化并用多轮 benchmark 对比。测试通过不等于学完了。

挑两关细看#

Level 03:少掉的积分#

20 名计分工人同时处理得分事件,每人登记 5,000 次一分,理论总分 100,000,但程序时不时会少分。

问题出在 score += points 不是一步完成的,而是读取、计算、写回三步。两个 goroutine 交错执行时:

TEXT
工人 A 读到 score = 41     工人 B 读到 score = 41
工人 A 算出 42             工人 B 算出 42
工人 A 写回 42             工人 B 写回 42   ← 丢了一分

这就是「丢失更新」。关卡 README 里有一句值得反复读:

普通运行偶尔得到正确分数并不能证明安全;反过来,错误总分展示了结果问题,却不能替代竞态检测。

所以验收标准是两条同时满足:结果精确等于 100,000,并且 go test -race 没有报告。也不能靠减少工人或者改成串行来「修好」,要找出最小的正确临界区。

Level 07:相反方向的转账#

A 转 B、B 转 A 几乎同时发生。代码先锁付款方、再锁收款方,于是两个 goroutine 各拿一把锁、互相等对方,死锁了。

死锁要同时满足四个条件:互斥、持有并等待、不可抢占、循环等待。前三个是锁本身的性质,能拆掉的只有「循环等待」。这一关的要求是:

  • 不按「付款方 / 收款方」的角色决定加锁顺序,而是按系统内唯一、不可变的账户 ID 建立全局顺序;
  • 拿到两把锁之后按业务方向改余额,再按加锁的相反顺序解锁;
  • 自己转给自己要单独处理:Go 的 sync.Mutex 不可重入,同一把锁锁两次会把自己卡死。

测试里设了 500 毫秒超时,失败时会明确报超时,而不是让整个测试进程挂住。两把锁之间那 20 毫秒的暂停只是为了稳定复现危险的交错,它不是根因,也不能靠删掉它来「修复」。

为什么值得这么练#

  • 先预测,逼你在脑子里把调度过一遍,比直接看答案记得牢;
  • -race 把「偶尔出错」变成「每次都报错」,这是 Go 并发最好用的工具;
  • 不给答案、分级提示,卡住本身就成了学习的一部分。

仓库地址:RichZDS/golangexe。建议从第一关开始,别跳关。