练习
学完本章你应该掌握
- 能识别哪些并发场景会发生数据竞争,并知道共享变量、切片、map 在并发写入时都需要同步保护。
- 能使用
sync/atomic安全地维护简单计数器,并理解为什么count++在并发场景下不可靠。 - 能使用
sync.Mutex保护共享临界区,写对Lock、Unlock和defer的基本范围。 - 能使用
sync.Map完成并发安全的存取、删除、遍历,并在取值后做必要的类型断言。 - 能把前面学过的结构体、方法、切片、map、函数、WaitGroup、channel、
select等知识自然组合到线程安全场景里。
简单
第 1 题:用原子操作统计完成的练习数
编写一个 Go 程序,完成下面要求:
- 准备切片:
| |
- 为每个任务启动一个 goroutine
- 每个 goroutine 输出
xxx 完成 - 使用
sync/atomic安全统计完成数量 - 使用
sync.WaitGroup等待所有 goroutine 结束 - 最后输出:
| |
查看参考答案
| |
说明:前面每一行 xxx 完成 的输出顺序不固定,但最终的 finished 应该稳定等于 4。
第 2 题:修正并发点赞数代码
下面程序的目标是稳定输出:
| |
但它现在有线程安全问题。请改正后让它正常运行。
| |
查看参考答案
修正后的代码如下:
| |
关键点:
likes++不是原子操作,它包含“读取旧值、加 1、写回新值”多个步骤。- 多个 goroutine 同时执行这几个步骤时,就可能把彼此的结果覆盖掉。
- 这里的共享数据只是一个简单计数器,用
atomic.AddInt64最直接。
第 3 题:给购物车总价加互斥锁
编写一个 Go 程序,完成下面要求:
- 定义结构体
Cart - 字段包括:
mu sync.MutexTotal int - 给它绑定方法
Add(price int) - 在
Add里用互斥锁保护Total - 在
main中启动两个 goroutine,分别调用:cart.Add(99)cart.Add(71) - 等两个 goroutine 都结束后,输出:
| |
查看参考答案
| |
说明:这里的共享数据不是单个计数器,而是结构体里的字段,所以更适合用 sync.Mutex 保护临界区。
一般
第 4 题:实现线程安全的词频统计器
编写一个 Go 程序,完成下面要求:
- 定义结构体
WordCounter - 字段包括:
mu sync.Mutexdata map[string]int - 给它绑定两个方法:
Add(word string)Get(word string) int - 准备下面这个切片:
| |
- 为每个单词启动一个 goroutine,调用
Add - 等全部 goroutine 完成后,按下面顺序输出统计结果:
| |
查看参考答案
| |
说明:这道题除了练习加锁,还顺手复习了结构体、方法、切片遍历和 map 计数。
第 5 题:使用 sync.Map 保存课程字符数
编写一个 Go 程序,完成下面要求:
- 准备切片:
| |
- 为每个课程名启动一个 goroutine
- 每个 goroutine 把“课程名 -> 字符数”存进
sync.Map - 等所有 goroutine 完成后:
- 使用
Load("接口")输出
- 使用
| |
- 删除
"切片" - 再次读取
"切片",如果不存在,输出
| |
- 使用
Range统计剩余课程名的总字符数,并输出
| |
查看参考答案
| |
说明:
Load取出的值类型是any,这里需要断言成int才能继续使用。- 这道题顺手复习了前面字符串章节里的
[]rune,因为题目要求按“字符数”而不是字节数来统计。
第 6 题:修正并发切片追加程序
下面程序的目标是:
- 每个 goroutine 往
logs里追加一条日志 - 最后输出日志总数
count=3
但它现在有线程安全问题。请改正后让它正常运行。
| |
查看参考答案
修正后的代码如下:
| |
说明:
append操作可能触发切片扩容和底层数组变更,所以并发追加也需要同步保护。- 最后三条日志的先后顺序不固定,只要总数是
3且每条日志都成功追加即可。
进阶
下面两题会把前面学过的结构体、方法、切片、map、WaitGroup、channel 和 select 一起串起来,但真正要解决的问题仍然是共享数据的线程安全。
第 7 题:实现并发订单统计中心
编写一个 Go 程序,完成下面要求:
- 定义结构体
Order,字段包括:User stringAmount int - 定义结构体
OrderCenter,字段包括:mu sync.Mutextotals map[string]intprocessed int64 - 给
OrderCenter绑定方法Record(order Order) - 在
Record中:- 用
Mutex安全更新每个用户的订单总金额 - 用原子操作安全统计已处理订单数
- 用
- 准备下面这组订单:
| |
- 为每笔订单启动一个 goroutine 调用
Record - 等全部处理完成后,输出:
| |
查看参考答案
| |
说明:这道题里两类共享数据的保护方式不同。
processed是简单计数器,适合用原子操作。totals是共享map,适合用互斥锁保护整段读写。
第 8 题:实现带超时的并发学习结果表
请完成一个稍微完整一点的小场景:
- 定义结构体
LessonResult,字段包括:Name stringStatus string - 准备切片:
| |
- 为每个课程启动一个 goroutine
- 每个 goroutine 先模拟学习耗时,再把结果写入
sync.Map sync.Map的 key 使用课程名,value 使用LessonResult- 另外准备一个
doneCh,每完成一个课程就发送一次完成信号 - 主 goroutine 使用
select同时等待:doneCh的完成信号time.After(1 * time.Second)的超时信号
- 如果全部课程都完成,就按
lessons的顺序输出:
| |
- 如果先超时,就输出:
| |
查看参考答案
| |
说明:
- 这里真正和本章强相关的是
sync.Map,因为多个 goroutine 会同时写入同一张结果表。 doneCh和select是前一章的知识,在这道题里只是自然地承担“完成通知”和“超时控制”的角色。