Go 定時器:Timer 和 Ticker
前言
在日常開發中,我們可能會遇到需要延遲執行或週期性地執行一些任務。這個時候就需要用到 Go 語言中的定時器。
在 Go 語言中,定時器類型有兩種:time.Timer 一次性定時器和 time.Ticker 週期性定時器。本文將會對這兩種定時器類型進行介紹。
準備好了嗎?準備一杯你最喜歡的咖啡或茶,隨着本文一探究竟吧。
Timer:一次性定時器
Timer 是一個一次性的定時器,用於在未來的某一時刻執行一次操作。
基本使用
創建 Timer 定時器的方式有兩種:
-
NewTimer(d Duration) *Timer:該函數接受一個time.Duration類型的參數d(時間間隔),表示定時器在過期之前等待的時間。NewTimer返回一個新的Timer定時器,這個定時器在其內部維護一個通道C,該通道在定時器被觸發時會接收當前的時間值。 -
AfterFunc(d Duration, f func()) *Timer:接受一個指定的時間間隔d和回調函數f。該函數返回一個新的Timer定時器,在定時器到期時直接調用f,而不是通過通道C發送信號。調用Timer的Stop方法可以停止定時器和取消調用f。
下面的代碼展示瞭如何使用 NewTimer 和 AfterFunc 來創建定時器以及定時器的基本用法:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/timer/usage.go
package main
import (
"fmt"
"time"
)
func main() {
// 使用 NewTimer 創建一個定時器
timer := time.NewTimer(time.Second)
gofunc() {
select {
case <-timer.C:
fmt.Println("timer 定時器觸發啦!")
}
}()
// 使用 AfterFunc 創建另一個定時器
time.AfterFunc(time.Second, func() {
fmt.Println("timer2 定時器觸發啦!")
})
// 主goroutine等待兩秒,確保看到定時器觸發的輸出
time.Sleep(time.Second * 2)
}
代碼運行結果如下所示:
timer 定時器觸發啦!
timer2 定時器觸發啦!
下面是代碼的逐步解析:
-
首先使用
NewTimer創建了一個定時器,然後在一個新的goroutine中監聽它的C屬性以等待定時器觸發。 -
其次,使用
AfterFunc創建另一個定時器,通過指定一個 回調函數 來處理定時器到期事件。 -
最後,主
goroutine等待足夠長的時間以確保定時器的觸發信息能夠被打印出來。
方法詳解
Reset
Reset(d Duration) bool:該方法用於重置 Timer 定時器的過期時間,也可以理解爲重新激活定時器。它接受一個 time.Duration 類型的參數 d,表示定時器在過期之前等待的時間。
除此之外,該方法還返回一個 bool 值:
-
如果定時器處於活動的狀態,返回
true。 -
如果定時器已經過期或被停止了,返回
false(false並不意味着激活定時器失敗,只是標識定時器的當前狀態)。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/timer/reset.go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(5 * time.Second)
// 第一次重置,定時器處於激活狀態,因此返回 true
b := timer.Reset(1 * time.Second)
fmt.Println(b) // true
second := time.Now().Second()
select {
case t := <-timer.C:
fmt.Println(t.Second() - second) // 1s
}
// 第二次重置,定時器已經處於過期狀態,因此返回 false
b = timer.Reset(2 * time.Second)
fmt.Println(b) // false
second = time.Now().Second()
select {
case t := <-timer.C:
fmt.Println(t.Second() - second) // 2s
}
}
代碼運行結果如下所示:
true
1
false
2
下面是代碼的逐步解析:
-
首先,創建了一個定時器,設置爲 5 秒後到期。
-
然後調用
Reset方法立即將其重置爲 1 秒後到期。因爲此時定時器仍處於激活狀態(即還未到期),所以Reset方法返回true。 -
接下來的
select語句等待定時器到期,並打印出實際經過的秒數(約等於 1 秒)。 -
接着第二次重置定時器,這次設置爲 2 秒後到期。由於定時器在這次重置時已經到期,
Reset方法返回false。 -
最後,再次使用
select語句等待定時器到期,並打印出這次經過的秒數(約等於 2 秒)。
Stop
Stop() bool:該方法用於停止定時器。如果定時器停止成功,返回 true,如果定時器已經過期或被停止,則返回 false。切記:Stop 操作不會關閉通道 C。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/timer/stop.go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(3 * time.Second)
// 停止定時器,在定時器觸發之前停止它,因此返回 true
stop := timer.Stop()
fmt.Println(stop) // true
stop = timer.Stop()
// 第二次停止定時器,此時定時器已經被停止了,返回 false
fmt.Println(stop) // false
}
代碼運行結果如下所示:
true
false
下面是代碼的逐步解析:
-
首先,創建了一個設置爲 3 秒後觸發的定時器。
-
然後立即調用
Stop方法停止定時器。因爲此時定時器還未觸發,所以Stop返回true。 -
最後再次調用
Stop方法嘗試停止同一個定時器。由於定時器已經被停止,這次Stop返回false。
Ticker:週期性定時器
Tciker 是一個週期性的定時器,用於在固定的時間間隔重複執行任務。它在每個間隔時間到來時,向其通道(Channel)發送當前時間。
基本使用
我們可以使用 NewTicker 函數來創建一個新的 Ticker 對象,該函數接受一個 time.Duration 類型的參數 d(時間間隔)。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/ticker/usage.go
package main
import (
"context"
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*5)
defer cancelFunc()
gofunc() {
for {
select {
case <-timeout.Done():
fmt.Println("timeout done")
return
case <-ticker.C:
fmt.Println("定時器觸發啦!")
}
}
}()
// 主goroutine等待 7 秒,確保看到定時器觸發的輸出
time.Sleep(time.Second * 7)
}
代碼運行結果如下所示:
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
timeout done
下面是代碼的逐步解析:
-
首先,創建了一個每秒觸發的定時器,確保函數週期結束後清理定時器,我們應該加上
defer ticker.Stop() -
然後,創建一個在 5 秒後超時的上下文。
cancelFunc被用於在退出前清理上下文。 -
接着,在一個新的
goroutine中,select語句用於監聽兩個通道:定時器的通道 (ticker.C) 和超時上下文的完成通道 (timeout.Done())。當定時器每秒觸發時,會打印出消息。當上下文超時(即 5 秒過後),打印出超時信息,並返回從而結束該goroutine。 -
最後,主
goroutine通過time.Sleep(time.Second * 7)等待 7 秒,以確保能夠觀察到定時器觸發和超時事件的輸出。
除了使用 select 語句監聽 ticker.C 以外,我們還可以使用 for range 的形式進行監聽:
for range ticker.C {}
需要注意的是,即使通過 Stop 方法停止 Ticker 定時器,其 C 通道不會被關閉。這意味着無論是通過 for select 還是 for range 去監聽 ticker.C,我們需要使用其他機制來退出循環,例如使用 context 上下文。
方法詳解
Reset
Reset(d Duration) 方法用於停止計時器並將其週期重置爲指定的時間。下一個時間刻度將在新週期結束後生效。它接受一個 time.Duration 類型的參數 d,表示新的週期。該參數必須大於零;否則 Reset 方法內部將會 panic。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/ticker/reset.go
package main
import (
"time"
)
func main() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// 重置定時器
ticker.Reset(1 * time.Second)
second := time.Now().Second()
for t := range ticker.C {
// 1s
fmt.Printf("週期:%d 秒", t.Second()-second)
break
}
}
代碼運行結果如下所示:
週期:1 秒
下面是代碼的逐步解析:
-
首先,創建了一個每 5 秒觸發一次的定時器
time.Ticker。 -
其次,使用
Reset方法重置定時器的觸發間隔。5 秒變成 1 秒。 -
最後通過一次循環,打印定時器的週期,預期結果爲 1 秒。
Stop
Stop() 方法用於停止定時器。在 Stop 之後,將不再發送更多的 tick 給其通道 C。切記:Stop 操作不會關閉通道 C。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/ticker/stop.go
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
quit := make(chanstruct{}) // 創建一個退出通道
gofunc() {
for {
select {
case <-ticker.C:
fmt.Println("定時器觸發啦!")
case <-quit:
fmt.Println("協程停止啦!")
return// 接收到退出信號,退出循環
}
}
}()
time.Sleep(time.Second * 3)
ticker.Stop() // 停止定時器
close(quit) // 發送退出信號
fmt.Println("定時器停止啦!")
}
代碼運行結果如下所示:
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
協程停止啦!
定時器停止啦!
-
首先,創建一個每秒觸發一次的
time.Ticker對象。同時,引入了一個類型爲chan struct{}的退出通道 quit。這個通道將用於向運行中的goroutine發送停止信號。 -
其次,啓動一個新的
goroutine。在這個goroutine中,使用for-select循環來監聽兩個事件:定時器的觸發(case <-ticker.C)和退出信號(case <-quit)。每當定時器觸發時,它會打印一條消息。如果收到退出信號,它會打印一條消息並退出循環。 -
接着,在主
goroutine中,time.Sleep(time.Second * 3)模擬了一段等待時間(3 秒),在這期間定時器會觸發幾次。 -
最後,主
goroutine通過調用Stop方法停止定時器,然後關閉退出通道。goroutine接收到退出信號後打印出一條消息並退出循環。
Stop 不會關閉其通道 C,因此我們需要藉助其他方式(例如退出信號)來清理資源。
Timer 和 Ticker 的主要區別
用途:
-
Timer用於單次延遲執行任務。 -
Ticker重複執行任務。
行爲特點:
-
Timer在設定的延遲時間過後觸發一次,發送一個時間值到其通道。 -
Ticker按照設定的間隔週期性地觸發,反覆發送時間值到其通道。
可控性:
-
Timer可以被重置(Reset方法)和停止(Stop方法)。Reset用於改變Timer的觸發時間。 -
Ticker可以被重置(Reset方法)和停止(Stop方法)。Reset用於改變Ticker觸發的時間間隔。
結束操作:
-
Timer的Stop方法用於阻止Timer觸發,如果Timer已經觸發,Stop不會從其通道中刪除已發送的時間值。 -
Ticker的Stop方法用於停止Ticker的週期性觸發,一旦停止,它不會再向通道發送新的值。
注意事項
-
無論是
Timer還是Ticker定時器,調用Stop方法之後,並不會關閉它們的C通道。如果有其他的goroutine在監聽這個通道,爲避免潛在的內存泄漏,需要手動結束該goroutine。通常,這種資源釋放的問題可以通過使用context或通過關閉信號(利用Channel實現)來解決。 -
當
Ticker定時器完成其任務後,爲了防止內存泄漏,應調用Stop方法來釋放相關資源。如果未及時停止Ticker,可能導致資源持續佔用。
小結
本文深入探討了 Go 語言中的 Timer 和 Ticker 定時器,詳細介紹了它們的創建方式、基本用法以及相關的方法等。此外,文章還概括了這兩個定時器之間的主要區別,並強調了在使用過程中的注意事項。
在編寫 Go 代碼時,我們應根據不同的應用場景去選擇合適的定時器。同時,我們應遵循良好的規範,特別是在定時器使用完畢後及時釋放資源,對於避免潛在的內存泄漏問題尤爲重要。
文章裏的所有代碼已上傳至:https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer
”
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IgXFofLEwhYYYUtIZ4_rTA