Go并发模型:超时,继续 (Go Concurrency Patterns: Timing out, moving on)
英文原版: https://blog.golang.org/concurrency-timeouts
并发编程有它自己的特点,比如超时。虽然go语言的channel没有直接支持,但是这些特点很容易被实现出来。假设我们想从一个channel读取数据,并且最多等他1秒钟,首先可以创建一个带信号的channel然后开一个goroutine先sleep1秒再往channel发数据:
timeout: = make(chan bool, 1)
go func() {
time.Sleep(1 * time.Second)
timeout <- true
}
然后可以用一个select语句来从channel接收数据或者收到超时信号。如果1秒后没有从channel收到数据,那么会执行超时逻辑,且不会再从channel读数据了。
select {
case <-ch:
// 从ch收到数据
case <-timeout:
// 收到超时信号
}
timeout这个通道是带有长度为1的缓冲区的,这样就允许timeout那个goroutine放完数据就可以退出,这个goroutine不知道也不关心数据是否被收到。也就是说,如果刚才那个select执行了第一个case(从ch读到了数据),这个timeout协程不会因此一直阻塞,实际上它最终会被GC回收。
(在上面例子中用的是
time.Sleep
来模拟延迟,在实际的项目中可以用
time.After
来直接创建一个带延迟的channel,这样就不用像上面那样创建完channel还得再开个goroutine。
time.After
的返回值就是channel,参数是超时时间,超时的时候可以从返回的channel读到Time。)
再来看看另一个例子,写个程序同时从多个数据库备份中读取数据,哪个先读到数据就用哪个数据。
比如写个
Query
方法,参数是多个数据库Conn的slice以及需要执行的sql字符串,在方法体中并行的向每个Conn发送请求,然后返回最先成功的那个:
func Query(conns []Conn, query string) Result {
ch := make(cahn Result)
for _, conn := range conns {
go func(c Conn) {
select {
case ch <- c.DoQuery(query):
default:
}
}(conn)
}
return <-ch
}
在这个例子中做了一个非阻塞式的send操作(
<-
)。把send操作放进带
default
的select中,如果这个send不能立刻执行,那么就会执行default逻辑,因此它是非阻塞的。这样就保证所有开起来的goroutine都不会被hang住。但是如果send操作执行在从channel读取之前,那么这次send也会失败。(注意这里所得send指的是
<-
本身,
<-
右边的方法调用会正常执行)
这个问题就是教科书上的
race condition
(竞争条件),但是要解决这个问题很简单,只需要在make这个channel的时候加个参数指定缓冲大小为1就行,这样就能保证第一个被send到channel的数据有地方放,同时也不要求代码的实际执行顺序。
上面这两个例子展示了在go中怎么用channel实现复杂的goroutine之间的交互。