迎向阳光 發表於 2021-7-28 07:38:00

go定时器--Ticker

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>1. 简介</li><li>2. 使用场景<ul><li>2.1 简单定时任务</li><li>2.2 定时聚合任务</li></ul></li><li>3. Ticker对外接口<ul><li>3.1 创建定时器</li><li>3.2 停止定时器</li><li>3.3 简单接口</li><li>3.4 错误示例</li></ul></li><li>4.实现原理<ul><li>4.1 数据结构</li><li>4.2实现原理<ul><li>4.2.1 创建Ticker</li><li>4.2.2 停止Ticker</li></ul></li></ul></li><li>总结</li><li>参考</li></ul></div><p></p>
<h2 id="1-简介">1. 简介</h2>
<p><strong>Ticker是周期性定时器</strong>,即周期性的触发一个事件,通过Ticker本身提供的管道将事件传递出去。</p>
<p>Ticker的数据结构与Timer完全一样</p>
<pre><code class="language-go">type Ticker struct {
    C &lt;- chan Time
    r runtimeTimer
}
</code></pre>
<p><strong>Ticker对外仅暴露一个channel,指定的时间到来时就往该channel中写入系统时间,也即一个事件。</strong></p>
<p>在创建Ticker时会指定一个时间,作为事件触发的周期。这也是Ticker与Timer的最主要的区别。</p>
<p>另外,ticker的英文原意是钟表的”滴哒”声,钟表周期性的产生”滴哒”声,也即周期性的产生事件</p>
<h2 id="2-使用场景">2. 使用场景</h2>
<h3 id="21-简单定时任务">2.1 简单定时任务</h3>
<p>有时,我们希望定时执行一个任务,这时就可以使用ticker来完成。<br>
下面代码演示,每隔1s记录一次日志:</p>
<pre><code class="language-go">// TickerDemo 用于演示ticker基础用法
func TickerDemo() {
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
      log.Println("Ticker tick.")
    }
}
</code></pre>
<p><code>for range ticker.C</code>会持续从管道中获取事件,收到事件后打印一行日志,如果管道中没有数据会阻塞等待事件,由于ticker会周期性的向管道中写入事件,所以上述程序会周期性的打印日志。</p>
<h3 id="22-定时聚合任务">2.2 定时聚合任务</h3>
<p>有时,我们希望把一些任务打包进行批量处理。比如,公交车发车场景:</p>
<ul>
<li>公交车每隔5分钟发一班,不管是否已坐满乘客;</li>
<li>已经坐满乘客情况下,不足五分钟也发车</li>
</ul>
<p>代码示例如下</p>
<pre><code class="language-go">// TickerLaunch用于演示ticker聚合任务用法
func TickerLaunch() {
    ticker := time.NewTicker(5 * time.Minute)
    maxPassenger := 30                   // 每车最大装载人数
    passengers := make([]string, 0, maxPassenger)

    for {
      passenger := GetNewPassenger() // 获取一个新乘客
      if passenger != "" {
            passengers = append(passengers, passenger)
      } else {
            time.Sleep(1 * time.Second)
      }

      select {
      case &lt;- ticker.C:               // 时间到,发车
            Launch(passengers)
            passengers = []string{}
      default:
            if len(passengers) &gt;= maxPassenger {// 时间没到,车已座满,发车
                Launch(passengers)
                passengers = []string{}
            }
      }
    }
}
</code></pre>
<p>上面代码中for循环负责接待乘客上车,并决定是否要发车。每当乘客上车,select语句会先判断ticker.C中是否有数据,有数据则代表发车时间已到,如果没有数据,则判断车是否已坐满,坐满后仍然发车。</p>
<h2 id="3-ticker对外接口">3. Ticker对外接口</h2>
<h3 id="31-创建定时器">3.1 创建定时器</h3>
<p>使用NewTicker()方法就可以创建一个周期性定时器,函数原型如下</p>
<pre><code class="language-go">func NewTicker(d Duration) *Ticker
</code></pre>
<p>其中参数<code>d</code>即为定时器时间触发的周期</p>
<h3 id="32-停止定时器">3.2 停止定时器</h3>
<p>使用定时器对外暴露的 Stop 方法就可以停掉一个周期性定时器, 函数原型如下</p>
<pre><code class="language-go">func (t *Ticker)Stop()
</code></pre>
<p>需要注意的是, 该方法会停止计时, 意味著不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完成后,生命周期结束后会自动释放。</p>
<p><strong>Ticker在使用完后务必要释放</strong>,否则会产生资源泄露,进而会持续消耗CPU资源,最后会把CPU耗尽。</p>
<h3 id="33-简单接口">3.3 简单接口</h3>
<p>部分场景下,启动一个定时器并且永远不会停止, 比如定时轮询任务, 此时可以使用一个简单的Tick函数来获取定时器的管道, 函数原型如下:</p>
<pre><code class="language-go">func Tick(d Duration) &lt;-chan Time
</code></pre>
<p>这个函数内部实际还是创建一个Ticker,但并不会返回出来,所以没有手段来停止该Ticker。所以,一定要考虑具体的使用场景。</p>
<h3 id="34-错误示例">3.4 错误示例</h3>
<p>Ticker 用于for循环时, 很容易出现意想不到的资源泄露问题</p>
<pre><code class="language-go">func WrongTicker() {
    for {
      select {
      case &lt;-time.Tick(1 * time.Second):
            log.Printf("Resource leak!")
      }
    }
}
</code></pre>
<p>上面代码,select每次检测case语句时都会创建一个定时器,for循环又会不断地执行select语句,所以系统里会有越来越多的定时器不断地消耗CPU资源,最终CPU会被耗尽。</p>
<h2 id="4实现原理">4.实现原理</h2>
<p>Ticker与之前讲的Timer几乎完全相同,无论数据结构和内部实现机制都相同,唯一不同的是创建方式。</p>
<p>Timer创建时,不指定事件触发周期,事件触发后Timer自动销毁。而Ticker创建时会指定一个事件触发周期,事件会按照这个周期触发,如果不显式停止,定时器永不停止。</p>
<h3 id="41-数据结构">4.1 数据结构</h3>
<p><strong>Ticker</strong><br>
Ticker数据结构与Timer除名字不同外完全一样。</p>
<p>源码包<code>src/time/tick.go:Ticker</code>定义了其数据结构:</p>
<pre><code class="language-go">type Ticker struct {
    C &lt;-chan Time // The channel on which the ticks are delivered.
    r runtimeTimer
}
</code></pre>
<p>Ticker只有两个成员:</p>
<ul>
<li>C: 管道,上层应用根据此管道接收事件;</li>
<li>r: runtime定时器,该定时器即系统管理的定时器,对上层应用不可见;</li>
</ul>
<p>这里应该按照层次来理解Ticker数据结构,Ticker.C即面向Ticker用户的,Ticker.r是面向底层的定时器实现。</p>
<p><strong>runtimeTimer</strong></p>
<p>runtimeTimer与Timer一样</p>
<h3 id="42实现原理">4.2实现原理</h3>
<h4 id="421-创建ticker">4.2.1 创建Ticker</h4>
<p>创建Ticker的实现,代码如下:</p>
<pre><code class="language-go">func NewTicker(d Duration) *Ticker {
    if d &lt;= 0 {
      panic(errors.New("non-positive interval for NewTicker"))
    }
    // Give the channel a 1-element time buffer.
    // If the client falls behind while reading, we drop ticks
    // on the floor until the client catches up.
    c := make(chan Time, 1)
    t := &amp;Ticker{
      C: c,
      r: runtimeTimer{
            when:   when(d),
            period: int64(d), // Ticker跟Timer的重要区就是提供了period这个参数,据此决定timer是一次性的,还是周期性的
            f:      sendTime,
            arg:    c,
      },
    }
    startTimer(&amp;t.r)
    return t
}
</code></pre>
<p>NewTicker()只是构造了一个Ticker,然后把Ticker.r通过startTimer()交给系统协程维护。<br>
其中period为事件触发的周期。</p>
<p>其中<code>sendTime()</code>方法便是定时器触发时的动作:</p>
<pre><code>func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) &lt;- Now():
    default:
    }
}
</code></pre>
<p>sendTime接收一个管道作为参数,其主要任务是向管道中写入当前时间。</p>
<p>创建<code>Ticker</code>时生成的管道含有一个缓冲区<code>(make(chan Time, 1))</code>,但是Ticker触发的事件却是周期性的,如果管道中的数据没有被取走,那么sendTime()也不会阻塞,而是直接退出,带来的后果是本次事件会丢失。</p>
<p>创建一个Ticker示意图如下:<br>
<img src="https://gitee.com/oneTotwo/images/raw/master/img/20210728073234.png" alt="" loading="lazy"></p>
<h4 id="422-停止ticker">4.2.2 停止Ticker</h4>
<p>停止Ticker,只是简单的把Ticker从系统协程中移除。函数主要实现如下:</p>
<pre><code class="language-go">func (t *Ticker) Stop() {
    stopTimer(&amp;t.r)
}
</code></pre>
<p>stopTicker()即通知系统协程把该Ticker移除,即不再监控。系统协程只是移除Ticker并不会关闭管道,以避免用户协程读取错误。</p>
<p>与Timer不同的是,Ticker停止时没有返回值,即不需要关注返回值,实际上返回值也没啥用途。</p>
<p>停止一个Ticker示意图如下:<br>
<img src="https://gitee.com/oneTotwo/images/raw/master/img/20210728073506.png" alt="" loading="lazy"></p>
<p>Ticker没有重置接口,也即Ticker创建后不能通过重置修改周期。</p>
<p><strong>需要格外注意的是Ticker用完后必须主动停止,否则会产生资源泄露,会持续消耗CPU资源。</strong></p>
<h2 id="总结">总结</h2>
<p>Ticker相关内容总结如下:</p>
<ul>
<li>使用time.NewTicker()来创建一个定时器;</li>
<li>使用Stop()来停止一个定时器;</li>
<li>定时器使用完毕要释放,否则会产生资源泄露;</li>
<li>NewTicker()创建一个新的Ticker交给系统协程监控;</li>
<li>Stop()通知系统协程删除指定的Ticker;</li>
</ul>
<h2 id="参考">参考</h2>
<ul>
<li>【专家编程】</li>
</ul>


</div>
<div id="MySignature" role="contentinfo">
    ♥永远年轻,永远热泪盈眶♥<br><br>
来源:https://www.cnblogs.com/failymao/p/15068712.html
頁: [1]
查看完整版本: go定时器--Ticker