Fork me on GitHub

Go语言开发-过程式编程-通信和并发语句-Select语句

5.4.1. Select语句

Go语言的select语句语法如下:

在select语句中Go语言会按顺序评估每一个发送和接收语句,如果任何语句可以继续进行(即没有被阻塞),则任意选择这些语句中一个来执行;如果没有可以继续执行的语句(即全部被阻塞),这可以分两种情况讨论,如果存在default语句块,则执行该default块中的语句且会从紧接着select语句的后面恢复执行,如果不存在default语句块,select语句将会阻塞直到至少有一个通信可以继续执行。

select语句的逻辑结果如下。在没有default语句块的情况下,select会被阻塞,且会在一个通信(接收或发送数据)发生时才会执行。如果存在default语句块,则select不会被阻塞且立即实行,这是因为可能有的case语句块有通信发生,或直接执行default语句块。

为了掌握这些语法,让我们来展示两个简短的例子。第一个例子是我们故意设计如此,但是却很好的说明了select语句的工作原理。第二个例子则更贴合实际使用。

在上面的代码片段中,我们创建了六个可以发送和接收布尔值的channel。然后,我们创建了一个具有无限循环的goroutine,在这个循环中,每次迭代都可以随机选择一个channel并发送一个true值。当然,该goroutine会立即阻塞,因为这些channel是没有缓冲的且我们还没有从它们中接收数据。

上面的代码片段,我们使用了六个channel来模拟一个骰子(严格地说,这是伪随机的)。 select语句会等待其中一个channel发送数据,这种情况下select语句是被阻塞的,因为没有default语句块,但只要有一个或多个channel准备好发送数据,其中一个case语句块就会被随机选择并执行。因为select语句在一个普通的循环中,其执行次数是有限制的。

现在让我们来看一个更贴合实际的例子。假设我们想要在两个单独的数据集上执行大量的计算任务,该计算会产生一系列结果。下面是执行此类计算的函数。

该函数接收要处理的数据和两个channel作为参数。answer channel用于将每个结果发送到监视代码,done channel用于通知监视代码计算已完成。

上面的代码创建了channel,开始耗时的计算,监视进度,并进行最后的清理工作,它们之间完全互不影响。

我们首先创建了answerα 和answerβ这两个channel来接收结果,其中一个channel用于跟踪计算何时完成。我们创建了defer关键字修饰的匿名函数,并在其中将channel关闭,这样一旦这两个channel不再需要时就可以将它们关闭,即外层函数返回时。接下来,我们提供数据并开始耗时的计算任务(在它们自己的goroutine中),每个goroutine都有自己的answer channel和共享的done channel用于通信。

我们本可以使用同一个answer channel,但如果我们这样做,我们就很难知道哪个结果由哪个数据给出(当然,这可能并不重要)。如果我们想要使用同一个channel,并想知道哪个数据会产生哪种结果,我们可以创建一个含有结果字段的struct结构体,例如:type Answer struct{id,answer int}.

随着耗时的计算任务开始于它们各自的goroutine(但被阻塞,因为是非缓冲channel),我们也已经准备好接收结果。for循环的每次迭代,which和result的值都会被重置,select语句会阻塞直至从可以执行的case子句中随机选择一个。如果其中一个answer已就绪,我们就重置which的值并将结果打印出来。如果done channel就绪,我们就将doneCount计数器的值加一,当doneCount的值等于我们要处理的数据数量时,计算任务完成,for循环结束。

一旦跳出for循环,我们就会知道这两个计算任务的goroutine将不再发送数据到它们的channel(因为它们在完成后从自身的无限for循环中跳了出来)。当函数返回时,defer代码块会将channel关闭并释放它们使用的资源。在这之后,垃圾收集器会清理goroutine本身,因为它们已不再执行且channel已被关闭。

Go语言的通信和并发是非常灵活且功能强大的,第7章将专门讨论这些问题。


目录


作者:Johnson
原创文章,版权所有,转载请保留原文链接。

发表评论

电子邮件地址不会被公开。 必填项已用*标注