前两天面试腾讯云平台开发的时候,面到最后,终于到了人皆吐槽的「手撕代码」环节。
小哥扔给我一道 Golang 编程题:
- 有一系列任务需要处理,最多 N 个并发;
- 只要有任务处理遇到错误,主程序就立即返回,输出对应错误信息;
- 等待所有任务执行成功。
代码框架如下:
1 | const N = 5 |
分析
这道题目综合考察了面试者对于 Golang 中协程并发调度、上下文通信、错误处理的编码熟练程度,还考察面试者是否掌握一些常见的 并发控制 套路,也称模式。
理解题目要求后,简单做下分析:
- 并发度:最多 N 个并发,不能有多少个任务就无脑地启动多少个协程。
- 只要有任务处理遇到错误,主程序结束,输出对应错误信息。这里我们很自然地会想到用
context
来控制父子协程。 - 等待所有任务执行成功:这里也涉及到协程之间的调度,主程序可以用
WaitGroup
也可以用channel
等待子协程返回任务的处理结果。
参考实现
1 | package main |
说明:
workerCh
控制并发度:- 初始化带 N 个缓冲的工作池
workerCh
- 从
workerCh
拿一个(若阻塞,说明所有 worker 都启动了,已达到并发上限 N),启动协程去处理任务。 - 任务完成后,重新唤醒
workerCh
,实现重用。
- 初始化带 N 个缓冲的工作池
使用
context
的WithCancel
来实现主程序控制子协程go-for-select
经典的多路监听:任务处理结果输出到respCh
,失败则马上cancel
,取消所有子协程主程序等待所有任务执行完成或遭遇错误:
<-waitCh
成功结果
1 | job:d result:<nil> |
失败结果
1 | wait all jobs finished or once error occurs... |
优化
上面的实现,虽然控制了程序中最多有 N 个并发,但它是每次都启动一个协程,只不过我们控制了不超过 N 个,任务处理完成后,这个协程就自动销毁了。
但频繁地创建/销毁协程也是一笔开销,可以考虑用 协程池
:初始化 N 个协程,放入协程池,每个协程去拉取消费任务列表里面的任务。
扩展
- 题目中的任务列表是固定的,如果任务列表也在动态变化,该如何实现?如果让面试者也实现任务的生产过程呢?
- 题目中并没有对任务处理进行超时控制,如果每个任务限制最长处理时间1s,该如何实现?并保证所有任务都能得到处理?