「这是我参与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 之间的复杂交互。