Go 博客

Go 并发模式:超时与前进

Andrew Gerrand
2010 年 9 月 23 日

并发编程有其自身的惯用语。一个很好的例子是超时。尽管 Go 的通道不直接支持超时,但它们很容易实现。假设我们想从通道 ch 接收值,但最多等待一秒钟。我们可以先创建一个信令通道,并启动一个 goroutine,该 goroutine 在向该通道发送之前会休眠。

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

然后我们可以使用一个 select 语句从 chtimeout 中接收。如果在等待一秒后 ch 没有值到达,则会选择 timeout 分支,并放弃从 ch 读取的操作。

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

timeout 通道的缓冲区大小为 1,允许 timeout goroutine 发送值到该通道然后退出。该 goroutine 并不知道(或关心)这个值是否被接收。这意味着如果 ch 在超时前接收到值,该 goroutine 也不会永远挂起。timeout 通道最终会被垃圾回收器回收。

(在此示例中,我们使用 time.Sleep 来演示 goroutine 和通道的机制。在实际程序中,您应该使用 time.After,这是一个返回一个通道并在指定持续时间后向该通道发送值的函数。)

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

函数 Query 接受一个数据库连接的切片和一个 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 分支的发送操作实现的。如果发送不能立即进行,则会选择 default 分支。非阻塞发送保证了循环中启动的 goroutine 不会挂起。然而,如果结果在主函数到达接收之前到达,发送可能会失败,因为没有接收方准备好。

这个问题是典型的教科书式的竞态条件示例,但修复方法很简单。我们只需要确保通道 ch 带缓冲区(通过在 make 函数的第二个参数中添加缓冲区长度),从而保证第一次发送有地方存放值。这样可以确保发送总是成功,并且无论执行顺序如何,都会检索到最先到达的值。

这两个示例展示了 Go 表达 goroutine 之间复杂交互的简单性。

下一篇文章:真正的 Go 项目:SmartTwitter 和 web.go
上一篇文章:Go Playground 简介
博客索引