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 上没有收到任何数据,则选择超时情况,并放弃从 ch 读取的尝试。

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

timeout 通道缓冲了 1 个值的空间,允许超时 goroutine 发送到通道然后退出。goroutine 不知道(也不关心)值是否被接收。这意味着如果在达到超时之前发生 ch 接收,goroutine 不会永远挂起。timeout 通道最终将由垃圾回收器释放。

(在这个例子中,我们使用 time.Sleep 来演示 goroutine 和通道的机制。在实际程序中,您应该使用 [time.After](/pkg/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 案例来实现。如果发送不能立即执行,则将选择默认情况。使发送非阻塞保证循环中启动的任何 goroutine 不会挂起。但是,如果结果在主函数到达接收之前到达,则发送可能会失败,因为没有人准备好接收。

这个问题是所谓的 竞争条件 的一个典型例子,但解决方法很简单。我们只需确保缓冲通道 ch(通过将缓冲区长度作为第二个参数添加到 make),从而保证第一个发送有一个地方放置值。这确保发送将始终成功,并且无论执行顺序如何,都会检索第一个到达的值。

这两个例子展示了 Go 可以多么简单地表达 goroutine 之间复杂的交互。

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