榴莲大亨 發表於 2021-7-27 07:44:00

Go定时器--Timer

<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>前言</li><li>Timer 定时器<ul><li>简介</li><li>使用场景<ul><li>1. 设定超时时间</li><li>2. 延迟执行某个方法</li></ul></li><li>Timer对外接口<ul><li>1. 创建定时器</li><li>2. 停止定时器</li><li>3. 重置定时器</li></ul></li><li>其他接口<ul><li>1. After()</li><li>2. AferFunc()</li></ul></li><li>总结</li></ul></li><li>Timer实现原理<ul><li>前言</li><li>数据结构<ul><li>1. Timer</li><li>2.runtimeTimer</li></ul></li><li>实现原理<ul><li>1. 创建Timer</li><li>2. 停止Timer</li><li>3. 重置Timer</li></ul></li><li>总结</li></ul></li><li>参考</li></ul></div><p></p>
<h2 id="前言">前言</h2>
<p>Go提供了两种定时器, 即 <strong>一次性定时器</strong>, <strong>周期定时器</strong></p>
<ul>
<li>一次性定时器:定时器只计时一次,结束便停止</li>
<li>周期定时器:定时器周期性进行计时</li>
</ul>
<p>本篇将快速介绍这两种定时器的基本用法,重点介绍其内部实现原理,最后再给出一个案例揭示使用定时器的风险。</p>
<h2 id="timer-定时器">Timer 定时器</h2>
<h3 id="简介">简介</h3>
<p>Timer实际上是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。之所以叫单一事件,是因为Timer只执行一次就结束,这也是Timer与Ticker的最重要的区别之一。</p>
<p>通过<code>src/time.sleep.go:Timer</code>定义了 Timer数据结构:</p>
<pre><code class="language-go"> // Timer代表一次定时,时间到来后仅发生一个事件。
type Timer struct {
    C &lt;-chan Time
    r runtimeTimer
}
</code></pre>
<p>Timer对外仅暴露一个channel,指定的时间到来时就往该channel中写入系统时间,也即一个事件。</p>
<h3 id="使用场景">使用场景</h3>
<h4 id="1-设定超时时间">1. 设定超时时间</h4>
<p>有时我们希望从一个管道中读取数据,在管道中没有数据时,我们不想让程序永远阻塞在管道中,而是设定一个超时时间,在此时间段中如果管道中还是没有数据到来,则判定为超时。</p>
<p>比如从一个连接中等待数据,其简单的用法如下代码所示:</p>
<pre><code class="language-go">func waitChannel(conn &lt;-chan string) bool {
    timer := time.NewTimer(1 * time.Second) //设置超时时间 1s
   
    select {
      case &lt;- conn:
      timer.stop() // 接收到数据后,要停止计时器
      return true
    case timer.C: //超时判断
      fmt.Println("WaitChnel timeout!")
      return false
    }
}
</code></pre>
<p>WaitChannel作用就是检测指定的管道中是否有数据到来,通过select语句轮询conn和timer.C两个管道,timer会在1s后向timer.C写入数据,如果1s内conn还没有数据,则会判断为超时。</p>
<h4 id="2-延迟执行某个方法">2. 延迟执行某个方法</h4>
<p>有时我们希望某个方法在今后的某个时刻执行,如下代码所示:</p>
<pre><code class="language-go">func DelayFunction() {
    timer := time.NewTimer(5 * time.Second)

    select {
    case &lt;- timer.C:
      log.Println("Delayed 5s, start to do something.")
      // do something
    }
}
</code></pre>
<p>DelayFunction()会一直等待timer的事件到来才会执行后面的方法(打印)。</p>
<h3 id="timer对外接口">Timer对外接口</h3>
<h4 id="1-创建定时器">1. 创建定时器</h4>
<p>使用方法,<code>func NewTimer(d Duration) *Timer</code> 指定一个时间即可创建一个Timer,Timer一经创建便开始集是, 不需要额外的启动命令。</p>
<p>实际上,创建Timer意味着把一个计时任务交给系统守护协程,该协程管理着所有的Timer,当Timer的时间到达后向Timer的管道中发送当前的时间作为事件。详细的实现原理我们后面会单独介绍。</p>
<h4 id="2-停止定时器">2. 停止定时器</h4>
<p>Timer创建后可以随时停止,停止计时器的方法是:</p>
<pre><code class="language-go">func (t *Timer) Stop() bool
</code></pre>
<p>其返回值代表定时器有没有超时:</p>
<ul>
<li>true: true: 定时器超时前停止,后续不会再有事件发送;</li>
<li>false: 定时器超时后停止;</li>
</ul>
<p>实际上,停止计时器意味着通知系统守护协程移除该定时器。详细的实现原理我们后面单独介绍。</p>
<h4 id="3-重置定时器">3. 重置定时器</h4>
<p>已过期的定时器或者已停止的定时器,可以通过重置动作重新激活,重置方法如下:</p>
<pre><code class="language-go">func (t *Timer) Reset(d Duration) bool
</code></pre>
<p>重置的动作实质上是先停掉定时器,再启动。其返回值也是掉计时器的返回值。</p>
<p>需要注意的是,<strong>重置定时器虽然可以用于修改还未超时的定时器,但正确的使用方式还是针对已过期的定时器或已被停止的定时器</strong>,同时其返回值也不可靠,返回值存在的价值仅仅是与前面版本兼容。</p>
<p>实际上,重置定时器意味着通知系统守护协程移除该定时器,重新设定时间后,再把定时器交给守护协程</p>
<h3 id="其他接口">其他接口</h3>
<p>前面介绍了Timer的标准接口,time包同时还提供了一些简单的方法,在特定的场景下可以简化代码。</p>
<h4 id="1-after">1. After()</h4>
<p>有时我们就是想等指定的时间,没有需求提前停止定时器,也没有需求复用该定时器,那么可以使用匿名的定时器。</p>
<p><code>func After(d Duration) &lt;-chan Time</code>方法创建一个定时器,并返回定时器的管道,如下代码所示:</p>
<pre><code class="language-go">func AfterDemo() {
    log.Println(time.Now())
    &lt;- time.After(1 * time.Second)
    log.Println(time.Now())
}
</code></pre>
<p><code>AfterDemo()</code>两条打印时间间隔为1s,实际还是一个定时器,但代码变得更简洁。</p>
<h4 id="2-aferfunc">2. AferFunc()</h4>
<p>前面我们例子中讲到延迟一个方法的调用,实际上通过AfterFunc可以更简洁。AfterFunc的原型为:</p>
<pre><code>func AfterFunc(d Duration, f func()) *Timer
</code></pre>
<p>该方法在指定时间到来后会执行函数f。例如:</p>
<pre><code>func AfterFuncDemo() {
    log.Println("AfterFuncDemo start: ", time.Now())
    time.AfterFunc(1 * time.Second, func() {
      log.Println("AfterFuncDemo end: ", time.Now())
    })

    time.Sleep(2 * time.Second) // 等待协程退出
}
</code></pre>
<p>AfterFuncDemo()中先打印一个时间,然后使用AfterFunc启动一个定器,并指定定时器结束时执行一个方法打印结束时间。</p>
<p>与上面的例子所不同的是,<strong><code>time.AfterFunc()</code>是异步执行的</strong>,所以需要在函数最后sleep等待指定的协程退出,否则可能函数结束时协程还未执行。</p>
<h3 id="总结">总结</h3>
<p>Timer内容总结如下:</p>
<ul>
<li>time.NewTimer(d)创建一个Timer;</li>
<li>timer.Stop()停掉当前Timer;</li>
<li>timer.Reset(d)重置当前Timer;</li>
</ul>
<h2 id="timer实现原理">Timer实现原理</h2>
<h3 id="前言-1">前言</h3>
<p>很多人想当然的以为,启动一个Timer意味着启动了一个协程,这个协程会等待Timer到期,然后向Timer的管道中发送当前时间。</p>
<p>实际上,每个Go应用程序都有一个协程专门负责管理所有的Timer,这个协程负责监控Timer是否过期,过期后执行一个预定义的动作,这个动作对于Timer而言就是发送当前时间到管道中。</p>
<h3 id="数据结构">数据结构</h3>
<h4 id="1-timer">1. Timer</h4>
<p>源码包src/time/sleep.go:Timer定义了其数据结构:</p>
<pre><code class="language-go">type Timer struct {
    C &lt;-chan Time
    r runtimeTimer
}
</code></pre>
<p>Timer只有两个成员:</p>
<ul>
<li>C: 管道,上层应用根据此管道接收事件;</li>
<li>r: runtime定时器,该定时器即系统管理的定时器,对上层应用不可见;</li>
</ul>
<p>这里应该按照层次来理解Timer数据结构,Timer.C即面向Timer用户的,Timer.r是面向底层的定时器实现。</p>
<h4 id="2runtimetimer">2.runtimeTimer</h4>
<p>创建一个Timer实质上是把一个定时任务交给专门的协程进行监控,这个任务的载体便是runtimeTimer,简单的讲,每创建一个Timer意味着创建一个runtimeTimer变量,然后把它交给系统进行监控。我们通过设置runtimeTimer过期后的行为来达到定时的目的。</p>
<p>源码包src/time/sleep.go:runtimeTimer定义了其数据结构:</p>
<pre><code class="language-go">type runtimeTimer struct {
    tb uintptr                        // 存储当前定时器的数组地址
    iint                              // 存储当前定时器的数组下标

    when   int64                        // 当前定时器触发时间
    period int64                        // 当前定时器周期触发间隔
    f      func(interface{}, uintptr)   // 定时器触发时执行的函数
    arg    interface{}                  // 定时器触发时执行函数传递的参数一
    seq    uintptr                      // 定时器触发时执行函数传递的参数二(该参数只在网络收发场景下使用)
}
</code></pre>
<h3 id="实现原理">实现原理</h3>
<p>一个进程中的多个Timer都由底层的一个协程来管理,为了描述方便我们把这个协程称为系统协程。</p>
<p>系统协程把runtimeTimer存放在数组中,并按照when字段对所有的runtimeTimer进行<strong>堆排序</strong>,定时器触发时执行runtimeTimer中的预定义函数f,即完成了一次定时任务。</p>
<h4 id="1-创建timer">1. 创建Timer</h4>
<p>创建Timer的实现,非常简单:</p>
<pre><code class="language-go">func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)// 创建一个管道
    t := &amp;Timer{ // 构造Timer数据结构
      C: c,               // 新创建的管道
      r: runtimeTimer{
            when: when(d),// 触发时间
            f:    sendTime, // 触发后执行函数sendTime
            arg:c,      // 触发后执行函数sendTime时附带的参数
      },
    }
    startTimer(&amp;t.r) // 此处启动定时器,只是把runtimeTimer放到系统协程的堆中,由系统协程维护
    return t
}
</code></pre>
<p>NewTimer()只是构造了一个Timer,然后把Timer.r通过startTimer()交给系统协程维护。</p>
<p>其中when()方法是计算下一次定时器触发的绝对时间,即当前时间+NewTimer()参数d。</p>
<p>其中sendTime()方法便是定时器触发时的动作:</p>
<pre><code class="language-go">func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) &lt;- Now():
    default:
    }
}
</code></pre>
<p>sendTime接收一个管道作为参数,其主要任务是向管道中写入当前时间。</p>
<p>创建Timer时生成的管道含有一个缓冲区(make(chan Time, 1)),所以Timer触发时向管道写入时间永远不会阻塞,sendTime写完即退出。</p>
<p>之所以sendTime()使用select并搭配一个空的default分支,是因为后面所要讲的Ticker也复用sendTime(),Ticker触发时也会向管道中写入时间,但无法保证之前的数据已被取走,所以使用select并搭配一个空的default分支,确保sendTime()不会阻塞,Ticker触发时,如果管道中还有值,则本次不再向管道中写入时间,本次触发的事件直接丢弃。</p>
<p>startTimer(&amp;t.r)的具体实现在runtime包,其主要作用是把runtimeTimer写入到系统协程的数组中,并启动系统协程(如果系统协程还未开始运行的话)。更详细的内容,待后面讲解系统协程时再介绍。</p>
<p>综上,创建一个Timer示意图如下:</p>
<p><img src="https://gitee.com/oneTotwo/images/raw/master/img/20210727073212.png" alt="" loading="lazy"></p>
<h4 id="2-停止timer">2. 停止Timer</h4>
<p>停止Timer,只是简单的把Timer从系统协程中移除。函数主要实现如下:</p>
<pre><code class="language-go">func (t *Timer) Stop() bool {
    return stopTimer(&amp;t.r)
}
</code></pre>
<p>stopTimer()即通知系统协程把该Timer移除,即不再监控。系统协程只是移除Timer并不会关闭管道,以避免用户协程读取错误。</p>
<p>系统协程监控Timer是否需要触发,Timer触发后,系统协程会删除该Timer。所以在Stop()执行时有两种情况:</p>
<ul>
<li>Timer还未触发,系统协程已经删除该Timer,Stop()返回false;</li>
<li>Timer已经触发,系统协程还未删除该Timer,Stop()返回true;</li>
</ul>
<p>综上,停止一个Timer示意图如下:</p>
<p><img src="https://gitee.com/oneTotwo/images/raw/master/img/20210727073536.png" alt="" loading="lazy"></p>
<h4 id="3-重置timer">3. 重置Timer</h4>
<p>重置Timer时会先把timer从系统协程中删除,修改新的时间后重新添加到系统协程中。</p>
<p>重置函数主要实现如下所示:</p>
<pre><code class="language-GO">func (t *Timer) Reset(d Duration) bool {
    w := when(d)
    active := stopTimer(&amp;t.r)
    t.r.when = w
    startTimer(&amp;t.r)
    return active
}
</code></pre>
<p>其返回值与Stop()保持一致,即如果Timer成功停止,则返回true,如果Timer已经触发,则返回false。</p>
<p>重置一个Timer示意图如下:</p>
<p><img src="https://gitee.com/oneTotwo/images/raw/master/img/20210727073729.png" alt="" loading="lazy"></p>
<p>由于新加的Timer时间很可能变化,所以其在系统协程的位置也会发生变化(按顺序排列)。</p>
<p>按照官方说明,Reset()应该作用于已经停掉的Timer或者已经触发的Timer,按照这个约定其返回值将总是返回false,之所以仍然保留是为了保持向前兼容,使用老版本Go编写的应用不需要因为Go升级而修改代码</p>
<p>如果不按照此约定使用Reset(),有可能遇到Reset()和Timer触发同时执行的情况,此时有可能会收到两个事件,从而对应用程序造成一些负面影响,使用时一定要注意。</p>
<h3 id="总结-1">总结</h3>
<ul>
<li>NewTimer()创建一个新的Timer交给系统协程监控;</li>
<li>Stop()通知系统协程删除指定的Timer;</li>
<li>Reset()通知系统协程删除指定的Timer并再添加一个新的Timer;</li>
</ul>
<h2 id="参考">参考</h2>
<p>地鼠编程</p>


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