Go 并发模式:Timing out、moving on
并发编程有它自己的套路和用法。超时处理是一个好例子。尽管 go 的 channel 没有直接支持超时处理,但是利用 channel 来实现这个功能是十分容易的。假设我们想从 channel ch 中接收数据,但是该数据要等待至多一秒后才能到达。首先,我们创建一个 channel timeout并启动一个 goroutine,该 goroutine 休眠一秒后才会向 timeout 发送数据:
timeout := make(chan bool, 1)
go func() {
time.Sleep(1 * time.Second)
timeout <- true
}()
我们可以使用 select 语句从 ch 或 timeout 中接收数据。如果一秒钟后 ch 中仍然没有数据到达,则认定为超时,放弃从 ch 中读取数据。
select {
case <-ch:
// 从 ch 中读取数据
case <-timeout:
// 从 ch 中读取数据超时
}
timeout 缓冲区大小为 1 ,这能让 goroutine 在发送数据后直接退出。这个 goroutine 不知道(或者是不在乎)发送到 channel 的值是否被接收。这意味着该 goroutine 不会因为在超时前从 ch 接收了数据就被一直阻塞。无论如何,timeout 最终都会由垃圾回收器回收并释放。
(在这个例子中,我们使用了 time.Sleep 来展示 goroutine 和 channel 的机制。但是在实际的编程中,你最好使用 time.After 来实现类似功能,这个函数返回一个 channel 并且经过了指定的时间后向该 channel 发送数据)
我们再看看这个模式的一个变形。在这个例子中,我们的程序同时从多个拷贝数据库中读取数据。程序仅需要一个答案(因为记录是相同的),它应当接收最早返回的结果。
Query 函数输入参数为一个数据库连接的切片和一个查询语句。这个查询语句会在所有数据库中并行执行,最终程序返回 ch 接收到的第一个结果:
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 来实现的。如果对于 ch 的发送不能立马执行,那么程序就会选择执行 default 分支。使用非阻塞式的发送可以保证在循环中创建的 goroutine 都不会被挂起。但是这里的代码有一个问题,如果结果在主函数执行完之前到达,那么这些发送可能都会失败,因为还没有人准备好接收。
这个问题是竞态条件的的经典案例,修复它也是很容易的。我们只需要确保 ch 使用缓冲区(通过 make 函数的第二个参数可以设置缓冲区的大小),保证 ch 有足够的位置来接收第一个发送的值即可。这样就能保证发送总是能够成功,而且不管 goroutine 的执行顺序怎样,总能接收并返回第一个到达的结果值。
这两个例子说明 go 可以简单地表达 goroutine 之间的复杂互动。
翻译自:go blog 《Go Concurrency Patterns: Timing out, moving on》