原文地址:Go Concurrency Patterns: Timing out, moving on
并发编程有自己的习语。超时是一个很好的例子。虽然 Go channel 不直接支持超时,但它很容易实现。假设我们想从 channel ch 中获取数据,但是最多只能等待一秒钟。那么我们应该在开始的时候创建一个传递信号的 channel,并在发送数据之前启动一个 goroutine:
timeout := make(chan bool, 1)
go func() {
time.Sleep(1 * time.Second)
timeout <- true
}()
然后我们可以使用 select 语句去从 ch 或 timeout 中接收数据。如果一秒内没有从 ch 中读取任何数据 ,那么 timeout 会被选中,而从 ch 尝试读取数据的操作会被丢弃掉:
select {
case <-ch:
// 从 ch 的读操作发生
case <-timeout:
// 从 ch 的读操作超时
}
timeout channel 使用了空间大小为 1 的缓冲,所以 timeout goroutine 可以发送消息然后退出。这个 goroutine 不知道 (也不关心) 它发送的数据是否被读取。这意味着即使 ch 读到数据的时间要早于超时,超时 goroutine 也不会被阻塞住。timeout channel 最终会被垃圾回收期回收掉。
(在这个例子里,我们使用 time.Sleep 来说明 goroutine 和 channel 的机制。在实际代码中,你可以使用 time.After 函数。这个函数返回一个 channel,并在确定时间后通过这个 channel 发送消息。)
让我们看下这个模式的另一个变种。在这个例子中,我们的程序从多个复制的数据库中同步读取数据。程序只需要其中一个结果,所以它应该接收返回的第一个结果。
函数 Query 接受存放一系列数据库链接的切片以及一个请求字符串。它并行请求每个数据库,并返回它获取到的第一个结果:
func Query(conns []Conn, query string) Result {
ch := make(chan Result)
for _, conn := range conns {
go func(c Conn) {
select {
case ch <- c.DoQuery(query):
default:
}
}(conn)
}
return <-ch
}
例子里的闭包执行了一个非阻塞发送。它是通过在 select 语句中使用 default case 来执行发送操作来实现的。如果正常发送操作无法立即执行,那么 default 部分会被选择执行。这样就保证了在 for 循环中启动的 goroutine 不会被挂起。不过如果结果在主函数进入接收 (return <-ch) 之前到达,那么发送操作会失败,因为接收端并没有做好准备。
这个问题是一个教科书例子,被称为竞态条件。我们这里可以很容易去解决它。我们可以把 channel ch 改为有缓存 channel,确保第一次 channel 写入操作能执行成功,这样也能保证第一次获取的结果可以被读取并返回。
这两个例子说明了 Go 如何简单地在 goroutine 间进行复杂交互的操作。
-- 作者:Andrew Gerrand