PHP轉Go系列 | 毫秒級別定時器的使用姿勢
說到定時器大家第一時間想到的肯定是 Crontab 計劃任務,利用 Linux 操作系統的定時調度來執行我們寫的業務腳本,但是它只能精確到秒級別,無法具體到毫秒級別的調度。如果說利用 PHP 的 Sleep 函數來實現調度,也只能到秒級別。那我們想要實現毫秒級別的調度就沒有辦法了嗎?辦法還是有的,PHP 異步高性能通信擴展 Swoole 中就提供了毫秒級別的定時器,我們可以安裝 Swoole 擴展來予以支持。但是有些朋友會覺得安裝 Swoole 擴展略顯麻煩,那還有沒有其他的方式呢?我的回答是「有」,這一次我就利用 PHP 中事件循環 Event 來實現毫秒級別的定時器,當然我也還會使用 Go 語言來實現一次毫秒級別定時器,順便對比一下兩者之間的區別。
剛剛提到使用 PHP 中的事件循環 Event 來實現毫秒級別的定時器,那么我們直接看下面這段代碼。首先是初始化 EventConfig 事件配置類,然后再構造一個事件基礎對象 $event_base,再創建一個 Event 事件對象,之后 add 添加到事件循環之中,最后執行 loop 事件循環操作。其中需要指定事件類型為 Event::TIMEOUT,如果只指定 Event::TIMEOUT 則會只回調執行一次,如果要持續執行還需指定 Event::PERSIST 類型。在剛才我們也說到 Swoole 中也實現了毫秒級別定時器,其實不瞞你說,它的實現原理也是基于 Event 事件循環來實現的,本質原理都是一致的,如果有疑惑的朋友可以去看看 Swoole 的官方文檔。
<?php
/**
* 在多少毫秒之后執行
*/
function timer_after(int $msec, callable $callback){
// 初始化一個 EventConfig
$event_config = new EventConfig();
// 初始化一個 EventBase
$event_base = new EventBase($event_config);
// 初始化一個定時器事件
$timer = new Event($event_base, -1, Event::TIMEOUT, $callback);
// 將定時器添加到事件循環
$timer->add($msec/1000);
// 進入事件循環狀態
$event_base->loop();
}
/**
* 每間隔多少毫秒執行一次
*/
function timer_tick(int $msec, callable $callback_function){
// 初始化一個 EventConfig
$event_config = new EventConfig();
// 初始化一個 EventBase
$event_base = new EventBase($event_config);
// 初始化一個定時器事件
// Event::PERSIST 表示該事件將持續存在,一旦被觸發會再次激活
$timer = new Event($event_base, -1, Event::TIMEOUT | Event::PERSIST, $callback_function);
// 將定時器添加到事件循環
$timer->add($msec/1000);
// 進入事件循環狀態
$event_base->loop();
}
// 設定為 500 毫秒之后執行
timer_after(500, function(){
echo microtime(true) . " 只執行一次" . PHP_EOL;
// 這里可以執行具體的業務邏輯...
});
// 每間隔 500 毫秒執行一次
timer_tick(500, function(){
echo microtime(true) . " 每隔一段時間執行一次" . PHP_EOL;
// 這里可以執行具體的業務邏輯...
});看過了 PHP 中的實現方式,我們再來看看在 Go 語言中是怎樣實現毫秒級別定時器的。從下面這段代碼中可以看出,直接利用 time 標準庫中的 Ticker 便可以實現毫秒級別定時器的功能,首先使用 NewTicker 創建了一個定時器,然后使用 select case 語句來監聽 ticker.C 通道的數據,如果時間到了則會觸發,之后便可執行延時后的業務邏輯,最后一定要記得調用 ticker.Stop() 停止定時器。其中還想要持續觸發的話,只需要在外圍加上 for 循環就可以實現間隔執行。在這個例子中其實也可以看出 Go 語言中的定時器是基于 Channel 通道的超時來實現的,感興趣的朋友可以翻開源碼看一眼。
package test
import (
"time"
"fmt"
)
// 在指定時間之后只執行一次
func TestTickerAfter(t *testing.T) {
// 設置 500 毫秒之后執行
interval := 500 * time.Millisecond
// 創建定時器
ticker := time.NewTicker(interval)
// 確保在程序結束前停止定時器
defer ticker.Stop()
select {
case t := <-ticker.C: // 等待定時器觸發
fmt.Printf("只執行一次 %v\n", t.Local().UnixMilli())
// 這里可以執行具體的業務邏輯...
}
}
// 每間隔一段時間執行一次
func TestTickerTick(t *testing.T) {
// 設置每間隔 500 毫秒之后執行
interval := 500 * time.Millisecond
// 創建定時器
ticker := time.NewTicker(interval)
// 確保在程序結束前停止定時器
defer ticker.Stop()
for {
select {
case t := <-ticker.C: // 等待定時器觸發
fmt.Printf("每間隔一段時間執行一次 %v\n", t.Local().UnixMilli())
// 這里可以執行具體的業務邏輯...
}
}
}從上面的兩個例子可以看出 PHP 和 Go 中對毫秒定時器的實現原理是有所不同的,PHP 中是基于 Event 事件循環來實現的,這種回調式的實現方式在實際的編程中不順手,有些朋友可能會不太能適應,但是也沒有更好的方式了。那在 Go 中是基于 Channel 通道的超時機制來實現的,這種同步的編程方式會更符合編程的習慣。不過我個人認為,畢竟是兩門不同的語言,每種語言都有自己的風格,我們還是應該要求同存異,不應過分苛求。本次分享的內容就到這里了,希望對大家能有所幫助。























