天天看点

Go并发模型:超时,继续(Timing out, moving on译文)Go并发模型:超时,继续 (Go Concurrency Patterns: Timing out, moving on)

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之间的交互。