【译】Go并发模式:超时,继续

305 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

原文:Go Concurrency Patterns: Timing out, moving on

作者:Andrew Gerrand

链接:golang.google.cn/blog/concur…

并发编程中有一些常见的处理机制,如超时。尽管 Go 的通道 channel 并不直接支持它们,但它们很容易实现。假设我们希望从通道 ch 接收数据,但是只想要最多一秒钟的时间来等待值到达。我们可以先创建一个信号通道,然后启动一个协程 goroutine ,其在向通道发送数据前为睡眠状态:

timeout := make(chan bool, 1)
go func() {
    time.Sleep(1 * time.Second)
    timeout <- true
}()

然后,我们可以使用 select 语句从 chtimeout 接收。如果一秒钟后 ch 没有数据到达,则选择超时情况,并且放弃从 ch 读取数据的尝试。

select {
case <-ch:
    // a read from ch has occurred
case <-timeout:
    // the read from ch has timed out
}

timeout 通道包含一个值的缓冲空间,允许用来判断超时的协程发送数据到通道,然后退出。协程不知道(或关心)通道是否接收到该值。这意味着如果 ch 在超时之前接收数据, 协程将永远不会挂起。gc 最后会释放 timeout 通道。

(在这个例子中,我们使用 time.Sleep 来演示协程和通道的机制。在实际的程序中,你应该使用 time.After ,用来在指定的持续时间后发送数据到一个通道并将该通道返回。)

让我们看看这个模式的另一种变化。在本示例中,我们有一个同时读取多个重复数据库的程序。程序只需要其中一个答案,并且它应该接受最先到达的答案。

函数 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 语句中使用发送操作来实现。如果发送不能立即通过,将选择默认情况。使发送操作非阻塞可以保证循环中启动的协程不会挂起。然而,如果查询结果在 main 函数执行接收语句之前到达,那么发送可能会失败。

这个问题是竞争条件的教科书级示例,但是很容易修复。我们只需要给通道 ch 建立缓冲(通过向 make 添加缓冲区长度作为第二个参数),来保证第一个发送有存储数据值的位置。这将确保发送始终成功,并且无论执行顺序如何,都将检索到第一个到达的值。

这两个例子展示了 Go 可以通过简单的语句来表达协程间复杂的交互。