Go小技巧(二)— 打开已经关闭的channel

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go小技巧(二)— 打开已经关闭的channel相关的知识,希望对你有一定的参考价值。

参考技术A 有时候我们需要在完全可控的范围内复用channel,但是关闭了的channel原生语法并没有提供方法打开,所以利用指针再次打开。

channel的结构体在 chan.go 中:

Channel是否关闭取决于 hchan.closed ,0是打开,1是关闭。
方法:让指针指向 hchan.closed 直接修改它的值。

closedOffset为什么是28呢?这个涉及到struct对齐问题, Go内存优化(一)— struct对齐
在上面主要用了指针定位closed值,直接修改标志位。为了保证设置closed值的安全性所以在给它设置值的时候使用runtime.lock上锁。
关于 go:linkname 可以自行百度,在目录下创建一个 *.s 文件可以躲过编译时error: missing function body 。

channel使用

参考技术A 【译文】 原文地址
channel是Go语言的一个标志性特性,为go协程之间的数据交互提供一种非常强大的方式,而不需要使用锁机制。
本文将讨论channel的两个重要属性,一个是控制协程间数据发送和接收,以及对channel本身控制。

首先讨论下关闭的channel特性。一旦channel被关闭之后,就不能再继续发送数据给该channel,但是还是可以继续接收channel中的数据。如下所示:

output:

上述例子显示即使ch在for循环之前已经关闭,但还是可以正常的读取缓存中的true值,读完之后ok就会被赋值为false表示channel已经关闭,而且value值为对应channel类型bool的默认零值false。只要不停地从关闭的channel接收,就会无限的返回默认值和false。可以将for循环次数改大点试试即可验证。

通过以上例子可以发现,关闭的channel可以继续接收读取操作,这种特征是有用的。在使用range读取带缓存的channel时就会用到,一旦channel关闭,读取完缓存中数据就会停止接收数据退出。
将前面的例子改为如下:

output:

上面的例子就没有false打出来了。正好是写入channel里面的两个值。

channel与select结合更能发挥出其作用,让我们看一个例子:

上面的例子,因为finish在主协程中发送之后,马上就会在select中接收,并执行done.Done()。主协程wait马上会退出整个程序就结束。但是这里面存在一个问题,如果在select中没有添加finish case的话,主协程就永远发送不了数据到finish这个channel,因为其不带缓存。这里就可以通过将finish改成带缓存的channel,或者可以让select中的finish不会阻塞。

但是出现多个协程都在接收finish通道中的数据的话,就需要发送对应协程数量的值到channel中才能解决上面的问题。但是具体有多少个协程这往往是不好确定的,因为有些协程可能是程序其他部分创建的。一个比较好的选择就是通过使用关闭通道的方法来实现各协程能正常接收并结束。
如下所示:

output:

上面的例子就是使用了关闭的channel可以无限地接收到反馈数据。这样每个协程都能从finish通道中读到关闭信息并执行done.Done()使得主协程wait能退出。并且不需要关注多少个协程数,就能正确的让所有协程读到finish通道信息。
channel的这个特性,可以让程序员无需关注后台具体执行协程个数,确保每个协程都能接收到通道关闭信息,而无需担心死锁问题。

通过上面的例子我们也发现每个协程并不需要从通道中读取对应类型的数据,只需让接收操作能执行就行,让select不被阻塞。所以可以使用空结构体类型,我们可以改成如下:

这里我们只关注通道是否关闭这个信号,而不需要关注通道里面的数据,所以可使用空结构体类型通道。

第二个要讨论的是nil通道:如果定义了一个channel变量没有被初始化,或者被赋值为nil,那么该chennel总是处于阻塞状态。如下所示:

执行结果为:

因为channel为nil无法发送数据,当然也不能接收数据:

这个似乎看起来不是很重要,但是如果你想使用关闭channel来等待多个channel关闭的话,这个特性就有用处了。先看下面的例子:

WaitMany()函数看起来好像是一个等待通道a和b关闭的好方法,但是存在一个问题。假设a通道先关闭,case <-a就会变成非阻塞。因为bclosed还是false,程序就会进入到一个死循环当中,导致b通道永远无法确认关闭。
一个安全的方法就是使用nil通道总是阻塞的特点,如下所示:

上面的例子我们在WaitMany函数当中,当a或者b关闭时,case可执行了将对应的通道赋值为nil,让其阻塞这样就可以等待另一个通道关闭。当nil通道是select语句的一部分时,它会被有效地忽略,因此nil通道a会从select中删除它,只留下b,直到它被关闭,退出循环。

总之,closed和nil通道的简单属性对写出优质的go程序是很有用的,可以用来创建高并发程序。

以上是关于Go小技巧(二)— 打开已经关闭的channel的主要内容,如果未能解决你的问题,请参考以下文章

关闭Xshell警告音等一些小技巧

mac电脑小技巧:快速强制关闭苹果Mac电脑后台程序

[go] 检查 channel 是否已经被关闭

[译] Go语言测试进阶版建议与技巧

SecureCRT小技巧

Windows小技巧12--永久关闭开机弹出的今日热点