一个人说起自己时,便会得意忘形。——梅里美《卡门》
1. 前言
为什么超时需要控制,这个大家知道吗?自己在脑瓜子里面回想下曾经写过的超时控制代码。那么今天就给大家带来一篇超时控制实战篇,助你在项目中得心应手,废话不多说,直接上干货。
2. 为什么超时需要控制
- 请求时间过长,用户侧可能已经离开本页面了,服务端还在消耗资源处理,得到的结果没有意义。
- 过长时间的服务端处理会占用过多资源,导致并发能力下降,甚至出现不可用事故。
golang中一般一个请求是由多个串行或并行的子任务来完成的,每个子任务可能是另外的内部请求,那么当这个请求超时的时候,我们就需要快速返回,释放占用的资源,比如goroutine,文件描述符等。
3. 平民版逻辑
func Do(job interface{}) error { //模拟超时任务或者请求 time.Sleep(time.Minute) return nil } func requestDo(ctx context.Context, job interface{}) error { return Do(job) }
这样的实现可能用户无法忍受,因为一旦调用Do发生超时,用户侧就会等待很长一段时间没有响应,进而用户带着抱怨离开了页面,真是难过啊。
4. 精英版逻辑
func requestDo(ctx context.Context, job interface{}) error { ctx, cancel := context.WithTimeout(ctx, time.Second*2) defer cancel() done := make(chan error) go func() { //在这里调用超时控制请求或者任务 done <- Do(job) }() select { case err := <-done: //请求返回 return err case <-ctx.Done(): //超时控制 return ctx.Err() } }
大家发现问题了吗,亲爱的伙伴们,带着你们的慧眼,识别代码的bug,来来来。
好,那我给大家说下哪里有问题:
如果超时被控制返回了 即命中case <-ctx.Done(),那么直接就结束当前requestDo协称,但是我们的Do协称还在继续执行呢,当它准备返回的时候发现done没有goroutine接受了,那么Do协称就会一直卡在那里,导致协称泄漏啊
5. 超人版逻辑
解决上面bug的方案之一就是给done初始化一个size大小的空间。
done := make(chan error, 1)
这样的话,Do协称超时返回了,自动把结果写入done中,然后Do协称会自动退出,这样协称就不会泄漏了。
那有人会问,写进去之后会不会有影响啊,毕竟done没有人去接受了,那么这里明确告诉大家没有影响的,chan毕竟是一个对象,golang的channel资源是可以自动GC掉的。
6. 如果Do发生panic了呢
如果Do发生panic,此时就会发现panic是无法被捕获的,原因是因为在 requestDo内部起的goroutine里产生的panic其它goroutine无法捕获。
解决办法就是:requestDo里加上panicChan来处理,同样,需要 panicChan的buffer size为1,如下
panicChan := make(chan interface{}, 1)
详细请看:
func requestDo(ctx context.Context, job interface{}) error { ctx, cancel := context.WithTimeout(ctx, time.Second*2) defer cancel() done := make(chan error) panicChan := make(chan interface{}, 1) go func() { defer func() { if p := recover(); p != nil { panicChan <- p } }() done <- Do(job) }() select { case err := <-done: return err case p := <-panicChan: panic(p) //这里也需要panic case <-ctx.Done(): return ctx.Err() } }
改完就可以在requestDo的调用方处理panic 了。
7. 小结
大家看到了吧,通过以上知识点的学习,我相信大家可以写出更加健全的超时控制逻辑了,那还在等什么,快快实践吧。
8. 关注公众号
微信公众号:堆栈future