大家好,我是江湖十年。

今天继续翻译 Go 官方博客。这次是 2010 年发表的《Go Concurrency Patterns: Timing out, moving on》,文中用两个简短的例子,展示了如何借助 channel、select 和 goroutine 实现超时,以及如何处理并发查询中的竞态条件。本文是对这篇文章的中文翻译。

如下为译文:

原文地址:https://go.dev/blog/concurrency-timeouts

Andrew Gerrand

2010 年 9 月 23 日

并发编程有其自己的惯用法。超时就是一个很好的例子。虽然 Go 的 channel 并不直接支持超时,但实现起来很容易。假设我们想从 channel ch 接收值,但最多只愿意等待一秒。我们可以先创建一个用于发信号的 channel,并启动一个 goroutine,让它在休眠一段时间后再向该 channel 发送值:

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

然后,我们可以使用 select 语句从 ch 或 timeout 中接收值。如果一秒后 ch 上仍没有值到达,就会选中超时分支,并放弃从 ch 读取的尝试。

1
2
3
4
5
6
select {
case <-ch:
// a read from ch has occurred
case <-timeout:
// the read from ch has timed out
}

timeout channel 带有可容纳 1 个值的缓冲区,这让超时 goroutine 能够向该 channel 发送值,然后退出。这个 goroutine 并不知道(也不关心)值是否被接收。这意味着,如果 ch 的接收在超时到达之前就发生了,这个 goroutine 也不会永远滞留。timeout channel 最终会被垃圾回收器释放。

(在这个例子中,我们使用 time.Sleep 来演示 goroutines 和 channels 的工作机制。在实际程序中,你应该使用 time.After,这个函数会返回一个 channel,并在指定的时长过后向该 channel 发送值。)

我们来看这个模式的另一种变体。在这个例子中,程序会同时从多个副本数据库中读取数据。程序只需要其中一个答案,并且应该接受最先到达的那个答案。

Query 函数接收一个数据库连接切片和一个 query 字符串。它会并行查询每个数据库,并返回最先收到的响应:

1
2
3
4
5
6
7
8
9
10
11
12
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
}

在这个例子中,闭包执行的是非阻塞发送,这是通过把发送操作放进带有 default 分支的 select 语句实现的。如果发送不能立即完成,就会选中 default 分支。让发送变成非阻塞,可以保证循环中启动的 goroutines 都不会滞留。然而,如果结果在主函数执行到接收操作之前就到达了,由于此时还没有人准备好接收,这次发送就可能会失败。

这个问题是所谓竞态条件的典型示例,但修复起来很简单。我们只需确保 channel ch 带有缓冲区(把缓冲区长度作为第二个参数传给 make),从而保证第一个发送有地方存放值。这样就能确保发送总是成功,而且无论执行顺序如何,最先到达的值都会被取回。

这两个例子展示了 Go 表达 goroutines 之间复杂交互是多么简单。

原文完。

希望此文能对你有所启发。

延伸阅读

联系我