1 超时返回时的陷阱
func doBadthing(done chan bool) {
time.Sleep(time.Second)
done <- true
}
func timeout(f func(chan bool)) error {
done := make(chan bool)
go f(done)
select {
case <-done:
fmt.Println("done")
return nil
case <-time.After(time.Millisecond):
return fmt.Errorf("timeout")
}
}
// timeout(doBadthing)
上述代码是一个典型的实现超时的例子。
- 利用
time.After
启动了一个异步的定时器,返回一个 channel,当超过指定的时间后,该 channel 将会接受到信号。 - 启动了子协程执行函数 f,函数执行结束后,将向 channel
done
发送结束信号。 - 使用 select 阻塞等待
done
或time.After
的信息,若超时,则返回错误,若没有超时,则返回 nil。
如果每次调用,函数 f 都能够在超时前正常结束,那么启动的子协程(goroutine)能够正常退出。那如果是超时场景呢?子协程能够正常退出么?
done
是一个无缓冲区的 channel,如果没有超时,doBadthing
中会向 done 发送信号,select
中会接收 done 的信号,因此 doBadthing
能够正常退出,子协程也能够正常退出。
但是,当超时发生时,select 接收到 time.After
的超时信号就返回了,done
没有了接收方(receiver),而 doBadthing
在执行 1s 后向 done
发送信号,由于没有接收者且无缓存区,发送者(sender)会一直阻塞,导致协程不能退出。
如何避免
创建有缓冲区的 channel
即创建channel done
时,缓冲区设置为 1,即使没有接收方,发送方也不会发生阻塞。
func timeoutWithBuffer(f func(chan bool)) error {
done := make(chan bool, 1)
go f(done)
select {
case <-done:
fmt.Println("done")
return nil
case <-time.After(time.Millisecond):
return fmt.Errorf("timeout")
}
}
func TestBufferTimeout(t *testing.T) {
for i := 0; i < 1000; i++ {
timeoutWithBuffer(doBadthing)
}
time.Sleep(time.Second * 2)
t.Log(runtime.NumGoroutine())
}
使用 select 尝试发送
func doGoodthing(done chan bool) {
time.Sleep(time.Second)
select {
case done <- true:
default:
return
}
}
func TestGoodTimeout(t *testing.T) { test(t, doGoodthing) }