1 超时返回时的陷阱

  1. func doBadthing(done chan bool) {
  2. time.Sleep(time.Second)
  3. done <- true
  4. }
  5. func timeout(f func(chan bool)) error {
  6. done := make(chan bool)
  7. go f(done)
  8. select {
  9. case <-done:
  10. fmt.Println("done")
  11. return nil
  12. case <-time.After(time.Millisecond):
  13. return fmt.Errorf("timeout")
  14. }
  15. }
  16. // timeout(doBadthing)

上述代码是一个典型的实现超时的例子。

  • 利用 time.After 启动了一个异步的定时器,返回一个 channel,当超过指定的时间后,该 channel 将会接受到信号。
  • 启动了子协程执行函数 f,函数执行结束后,将向 channel done 发送结束信号。
  • 使用 select 阻塞等待 donetime.After 的信息,若超时,则返回错误,若没有超时,则返回 nil。

如果每次调用,函数 f 都能够在超时前正常结束,那么启动的子协程(goroutine)能够正常退出。那如果是超时场景呢?子协程能够正常退出么?

done 是一个无缓冲区的 channel,如果没有超时,doBadthing 中会向 done 发送信号,select 中会接收 done 的信号,因此 doBadthing 能够正常退出,子协程也能够正常退出。
但是,当超时发生时,select 接收到 time.After 的超时信号就返回了,done 没有了接收方(receiver),而 doBadthing 在执行 1s 后向 done 发送信号,由于没有接收者且无缓存区,发送者(sender)会一直阻塞,导致协程不能退出。

如何避免

创建有缓冲区的 channel

即创建channel done 时,缓冲区设置为 1,即使没有接收方,发送方也不会发生阻塞。

  1. func timeoutWithBuffer(f func(chan bool)) error {
  2. done := make(chan bool, 1)
  3. go f(done)
  4. select {
  5. case <-done:
  6. fmt.Println("done")
  7. return nil
  8. case <-time.After(time.Millisecond):
  9. return fmt.Errorf("timeout")
  10. }
  11. }
  12. func TestBufferTimeout(t *testing.T) {
  13. for i := 0; i < 1000; i++ {
  14. timeoutWithBuffer(doBadthing)
  15. }
  16. time.Sleep(time.Second * 2)
  17. t.Log(runtime.NumGoroutine())
  18. }

使用 select 尝试发送

  1. func doGoodthing(done chan bool) {
  2. time.Sleep(time.Second)
  3. select {
  4. case done <- true:
  5. default:
  6. return
  7. }
  8. }
  9. func TestGoodTimeout(t *testing.T) { test(t, doGoodthing) }