Go并发模式:Timing out, moving on

142 阅读2分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

并发编程有它自己的编码风格。 一个很好的例子是超时。 虽然 Go 的通道不直接支持它们,但它们很容易实现。 假设我们想从通道 ch 接收,但希望最多等待一秒钟以使值到达。 我们将首先创建一个信号通道并启动一个在通道上发送之前休眠的 goroutine:

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

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

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

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

(在这个例子中,我们使用 time.Sleep 来演示 goroutine 和通道的机制。在实际程序中,您应该使用 time.After,一个在指定的超时时间之后返回通道的方法。)

让我们看一下这种模式的另一种变体。 在这个例子中,我们有一个同时从多个副本数据库中读取的程序。 程序只需要一个返回值,它应该接受最先到达的返回值。

函数 Query 参数是数据库连接切片和查询的字符串sql。 它并行查询每个数据库并返回它收到的第一个响应:

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),则发送可能会失败,因为没有人准备好。

这个问题是所谓的竞态条件的教科书示例,但修复是很简单的。 我们只需确保缓冲通道 ch(通过添加缓冲区长度,也就是 make 的第二个参数),确保第一次发送有放置接收值的位置。 这确保了发送将始终成功,并且无论执行顺序如何,都会检索到第一个到达的值。

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