有个性的哪吒 發表於 2020-2-23 16:06:00

go 优雅的检查channel关闭

<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">原文作者:shitaibin
链接:https://www.jianshu.com/p/79d27f200bcf
來源:简书</pre>
</div>
<div>
<p>goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。这篇文章介绍,如何合理的退出goroutine,减少软件bug。<br>
goroutine在退出方面,不像线程和进程,不能通过某种手段<strong>强制</strong>关闭它们,只能等待goroutine主动退出。但也无需为退出、关闭goroutine而烦恼,下面就介绍3种优雅退出goroutine的方法,只要采用这种最佳实践去设计,基本上就可以确保goroutine退出上不会有问题,尽情享用。</p>
<h2>第一种:使用for-range退出</h2>
<p><code>for-range</code>是使用频率很高的结构,常用它来遍历数据,<strong><code>range</code>能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束</strong>,接着退出for循环。<br>
它在并发中的使用场景是:当协程只从1个channel读取数据,然后进行处理,处理后协程退出。下面这个示例程序,当in通道被关闭时,协程可自动退出。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">go func(in &lt;-chan int) {
    // Using for-range to exit goroutine
    // range has the ability to detect the close/end of a channel
    for x := range in {
      fmt.Printf("Process %d\n", x)
    }
}(inCh)
</pre>
</div>
<p>  </p>
<h2>第二种:使用,ok退出</h2>
<p><code>for-select</code>也是使用频率很高的结构,select提供了多路复用的能力,所以for-select可以让函数具有持续多路处理多个channel的能力。<strong>但select没有感知channel的关闭,这引出了2个问题</strong>:1)继续在关闭的通道上读,会读到通道传输数据类型的零值,2)继续在关闭的通道上写,将会panic。问题2可使用的原则是,通道只由发送方关闭,接收方不可关闭,即某个写通道只由使用该select的协程关闭,select中就不存在继续在关闭的通道上写数据的问题。</p>
<p>问题1可以使用<code>,ok</code>来检测通道的关闭,使用情况有2种。<br>
第一种:<strong>如果某个通道关闭后,需要退出协程,直接return即可</strong>。示例代码中,该协程需要从in通道读数据,还需要定时打印已经处理的数量,有2件事要做,所有不能使用for-range,需要使用for-select,当in关闭时,<code>ok=false</code>,我们直接返回。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">go func() {
    // in for-select using ok to exit goroutine
    for {
      select {
      case x, ok := &lt;-in:
            if !ok {
                return
            }
            fmt.Printf("Process %d\n", x)
            processedCnt++
      case &lt;-t.C:
            fmt.Printf("Working, processedCnt = %d\n", processedCnt)
      }
    }
}()</pre>
</div>
<p>第二种:如果<strong>某个通道关闭了,不再处理该通道,而是继续处理其他case</strong>,退出是等待所有的可读通道关闭。我们需要<strong>使用select的一个特征:select不会在nil的通道上进行等待</strong>。这种情况,把只读通道设置为nil即可解决。</p>
<div class="_2Uzcx_">&nbsp;</div>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">go func() {
    // in for-select using ok to exit goroutine
    for {
      select {
      case x, ok := &lt;-in1:
            if !ok {
                in1 = nil
            }
            // Process
      case y, ok := &lt;-in2:
            if !ok {
                in2 = nil
            }
            // Process
      case &lt;-t.C:
            fmt.Printf("Working, processedCnt = %d\n", processedCnt)
      }

      // If both in channel are closed, goroutine exit
      if in1 == nil &amp;&amp; in2 == nil {
            return
      }
    }
}()
</pre>
</div>
<p>  </p>
<h2>第三种:使用退出通道退出</h2>
<p><strong>使用<code>,ok</code>来退出使用for-select协程,解决是当读入数据的通道关闭时,没数据读时程序的正常结束</strong>。想想下面这2种场景,<code>,ok</code>还能适用吗?</p>
<ol>
<li>接收的协程要退出了,如果它直接退出,不告知发送协程,发送协程将阻塞。</li>
<li>启动了一个工作协程处理数据,如何通知它退出?</li>
</ol>
<p><strong>使用一个专门的通道,发送退出的信号,可以解决这类问题</strong>。以第2个场景为例,协程入参包含一个停止通道<code>stopCh</code>,当<code>stopCh</code>被关闭,<code>case &lt;-stopCh</code>会执行,直接返回即可。</p>
<p>当我启动了100个worker时,只要<code>main()</code>执行关闭stopCh,每一个worker都会都到信号,进而关闭。如果<code>main()</code>向stopCh发送100个数据,这种就低效了。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func worker(stopCh &lt;-chan struct{}) {
    go func() {
      defer fmt.Println("worker exit")
      // Using stop channel explicit exit
      for {
            select {
            case &lt;-stopCh:
                fmt.Println("Recv stop signal")
                return
            case &lt;-t.C:
                fmt.Println("Working .")
            }
      }
    }()
    return
}</pre>
</div>
<h2>最佳实践回顾</h2>
<ol>
<li>发送协程主动关闭通道,接收协程不关闭通道。技巧:把接收方的通道入参声明为只读(<code>&lt;-chan</code>),如果接收协程关闭只读协程,编译时就会报错。</li>
<li>协程处理1个通道,并且是读时,协程优先使用<code>for-range</code>,因为<code>range</code>可以关闭通道的关闭自动退出协程。</li>
<li><code>,ok</code>可以处理多个读通道关闭,需要关闭当前使用<code>for-select</code>的协程。</li>
<li>显式关闭通道<code>stopCh</code>可以处理主动通知协程退出的场景。</li>
</ol></div><br><br>
来源:https://www.cnblogs.com/-wenli/p/12350181.html
頁: [1]
查看完整版本: go 优雅的检查channel关闭