在实现一些服务的过程中,我们需要对内部处理时间进行控制,以防客户端一直在等待响应。
select-case 实现的超时控制
在 go 中,利用 select + case + time 包,就可以很轻松实现超时控制。我们修改Go by Example: Timeouts中的一个例子:
1 | package main |
在上面的例子中,从第 10 到第 22 行,我们创建了一个大小为 1 的 channel c1
,然后创建一个 goroutine。在这个 goroutine 中,等待 2 秒后,打印日志,并发送一条消息到 channel c1
中。接着,我们利用 select-case 实现超时时间为 1 秒的超时控制。在第一个 case
中,等待来自 c1
的消息,并将此消息打印出来。在第二个 case
中,利用 time
包的 After
方法(这个方法在指定的时间间隔后,发送当前时间到返回的 channel 中),等待 1 秒后打印超时信息。
从第 24 到第 35 行,我们创建了另一个大小为 2 的 channel c2
,然后创建另一个 goroutine。在这个 goroutine 中,同样等待 2 秒后打印日志,并发送一条消息到 channel c2
中。接着,利用另一个 select-case 实现超时时间为 3 秒的超时控制。在第一个 case
中,等待来自 c1
的消息,并将此消息打印出来。在第二个 case
中,利用 time
包的 After
方法,等待 3 秒后打印超时信息。
运行得到输出如下: 1
2
3
4timeout 1
get result 1
get result 2
result 2
1 |
|
在上面的例子中,从第 10 到 13 行,我们声明了一个类型为 Mutext
的全局锁 mutex
和一个全局变量 id
。前者用以解决后者的同步写冲突。接下来的第 15 到 21 行,定义了一个函数 dosomething
,这个函数等待 1 秒后对变量 id
进行设值。
程序主入口处,我们依次创建 3 个 goroutine,每个 goroutine 都调用了 dosomething
函数进行设值。函数执行结束后,通过外部的 channel done
来通知调用者。接下来,在第 31 到 36 行,利用 select-case 进行超时控制,超时时间为当前索引指定的秒数。为了更清楚地看出耗时,我们在日志打印中加上了时间打印。
运行会发现,第 2 个请求因为第 1 个请求尚未返回导致没有释放锁,从而超时。而接下来的第 3 个请求也因为同样的原因超时了: 1
2
32009-11-10 23:00:00 +0000 UTC m=+0.000000001 timeout 0
2009-11-10 23:00:01 +0000 UTC m=+1.000000001 timeout 1
2009-11-10 23:00:03 +0000 UTC m=+3.000000001 timeout 2
加上 context
如何?
在上面的例子中,上次请求超时对下次请求,甚至是下下次请求会发生影响。而这种影响是可以减轻或者避免的。我们可以使用 context 包来处理这种问题。
context
中有两个方法: * func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
:返回参数 parent
的一个拷贝,并且调整该拷贝的截止时间至不超过 d
指定的时间。如果 parent
的截止时间比 d
早,那么该拷贝语义上等同于 parent
。当截止时间过期时,或者调用了返回的 CancelFunc
,又或者 parent
的 Done
channel 被关闭了,这三种情况之一发生了,返回的 context 的 Done channel 就会被关闭。注意,关闭该 context 会释放其相关资源,因此,只要在这个 context 上的操作完成了,就必须立即调用 CancelFunc
。 * func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
:返回 WithDeadline(parent, time.Now().Add(timeout))
现在,我们使用 WithTimeout
方法来改进上面的例子。
1 | package main |
在第 15 行至 28 行,我们修改 dosomething
的函数签名,将 context.Context
类型的参数作为函数的第一个参数。然后,在该函数中,利用 select-case 和这个参数的 Done
方法来判断是否退出。在第 35 行到第 37 行,调用 context.WithTimeout
方法创建一个 context.Context
对象,超时时间为该 goroutine 的超时时间。然后将其传给 dosomething
函数。
运行得到以下输出: 1
2
3
42009-11-10 23:00:00 +0000 UTC m=+0.000000001 timeout 0
2009-11-10 23:00:01 +0000 UTC m=+1.000000001 timeout 1
2009-11-10 23:00:01 +0000 UTC m=+1.000000001 op timeout 1
2009-11-10 23:00:02 +0000 UTC m=+2.000000001 true 2
总结
第一次在 go 中实现超时控制的时候,满篇的 select-case,粗糙地在超时的时候返回而不管尚在执行中的 goroutine 的死活。结果是,大批量调用受到几个调用超时的影响,一直超时无法恢复。
context
这个包就在这种情况下出现在我的视线中。按惯例,context.Context
对象应该作为函数的第一个参数,并且不建议将其当成结构体的一个部分。此外,它还可以用来传递值等等。
但是,如果只是为了进行超时控制,而不得不把所有的函数方法都加上这个参数的话,总感觉不那么漂亮。希望未来 go 可以更好地更漂亮地解决超时退出下 goroutine 的退出问题。