~/posts/go-lock-bootcamp
Go 锁机制情景训练营:12 关,从 goroutine 打到 CAS
学并发最怕「看懂了」:读完 sync.Mutex 的文档觉得很简单,一写就出数据竞争。golangexe 换了个练法:先预测,再运行;先自己改,再要提示。仓库里没有参考答案。
玩法#
每一关是一个目录,里面有 README.md(场景和任务)、main.go(有问题的代码)和 main_test.go(验收测试):
- 按顺序进一关,读 README;
- 运行前先写下预测:结果对不对?每次都一样吗?
- 从项目根目录运行;
- 只改
main.go,不许改测试来绕过要求; - 普通测试和竞态检测都要通过。
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 交错执行时:
工人 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。建议从第一关开始,别跳关。