TAT.vorshen 深入學習定時器
In 未分類 on 2021年03月02日 by view: 8,667
0

或許在生活中大家都討厭定時器,比如周一早上的鬧鐘、承諾老板第二天一早給報告的 deadline;但是在代碼的世界里,定時器扮演著不可或缺的角色:定時任務、超時判斷、幀同步等等。
那定時器的本質是什么?我們使用的定時能力背后又暗藏著什么玄機,請繼續往下看。

注意:由于博客系統問題導致排版有點異常,接受不了的可以看 https://github.com/vorshen/blog/blob/master/timer/index.md

目錄

定時能力需要什么

信號

POSIX Timer

多路復用

定時能力需要什么

javascript 的定時器能力應該是使用最為方便,默認的上下文捕獲,函數式編程。

我們可以把 setTimeout 的執行,拆解一下,主要是以下的流程。
setTimeout 執行依賴
主要有三個環節:

  1. 存放 callback
  2. 啟動一個倒計時
  3. 倒計時結束,取出存好的 callback,RUN!

BTW: JS 中定時器這么方便,不僅僅是 v8 的功勞,還需要執行環境 (eg: chrome、node) 給予支持。如果用 d8 去調試,會發現 setTimeout 并沒有定時執行。

核心需要解決 1,2 兩個問題,先看存放 callback,這里總結一下存放的特點:

  • 上層設置定時任務的順序是不確定的,而最終的執行是有順序的,這里涉及到排序行為
  • 設置定時器的動作可能是多次的
    滿足由上條件,我們可以使用一個小根堆的數據結構來存放 callback。

BTW: 也有一種時間輪的方案,libco 中采取時間輪方案。

那么該如何啟動一個倒計時的鐘擺呢?從編程語言層面是沒有倒計時相關 api 的,還好操作系統內核給了我們一些解決方案。
BTW: 就好比說到 Linux 上定時任務,大家基本上都會想到 crontab,這也是內核給我們的能力的一種表現。

內核中具體的時鐘能力如何實現,不是我們的重點,這會涉及到 CPU 時鐘中斷,再底層還有硬件相關,感興趣的同學可以自行查閱。我們重點放在代碼中如何去使用操作系統提供的時鐘能力。
對于程序來說,我們的訴求就是設定了一個時間,當該時間到達(可以理解為超時),內核可以通知到應用程序。那么有哪些通知方式呢?

信號方案

那么我們先看信號的方案,一說到信號,可能就會想到 alarm(sleep 走開),這里舉個簡單的??。

結果就不截圖了,代碼比較好理解,核心就是圍繞 SIGALRM 的監聽和觸發。
不過這里有一些問題,我們一一來看下
Q1: 精度問題,秒為精度,這太草了,肯定不能接受
A1: 不過我們可以用其他函數代替,比如 setitimer (精度為毫秒)

Q2: 無法多次調用 alrm
A2: 我們需要包裝一層,處理多次調用的情況。

不過上面兩個個問題還算好解決,針對以上兩個問題解法,這里有個改為 setitimer 優化版本,可見這里。
結果如下圖
setitimer 執行結果
可以看到精度提高了,并且支持了多次調用。

但是別高興的太早!問題還沒有結束!
Q3: 多線程情況下怎么辦?
A3: 信號在多線程下就是不靈活,一般做法需要用單獨的線程去監聽信號,其他線程屏蔽,寫起來很麻煩。

Q4: 信號可靠性?無論是 alrm 還是 setitimer 都是發送非實時信號。
A4: ???這太致命了,雖然是概率性的,但是總有在機場等艘船的感覺。

總結一下: 使用信號整體問題較多,雖然我們嘗試了一些解決方案,但是還是會存在無解的問題,所以這里也沒有真實使用信號的例子。

POSIX

針對剛剛的 Q1 到 Q4,根本性在于 alrm 和 setitmer 都不夠完善,為此 POSIX Timer 相關函數提供了解決方案。這一小節,我們主要看一下 POSIX Timer 相關函數,都是如何解決剛剛那些問題的。

  1. 精度問題
    POSIX Timer 支持程度更高,支持到納秒級別。

  2. 無法多次調用
    一個進程可以多次創建 Timer,相互獨立。

    可以看到這里并不需要自己去處理多次調用,直接走創建定時器,設置定時器的流程就行。

  3. 多線程
    POSIX Timer 提供了默認能力,當定時器結束的時候,可以啟動線程執行對應的函數。而且在 Linux 下,還擴展提供了往指定線程發送信號的能力。

  4. 信號可靠性
    POSIX Timer 也可以指定信號,不過不再局限于非實時信號,可以選擇實時信號,???。

針對 POSIX Timer 的調用,下面畫了一張圖
POSIX Timer 調用相關
具體函數使用、結構等可以看官方文檔,這里也給了一個簡易封裝的例子 posix 封裝為 setTimeout。

BTW: 其實本質上 POSIX Timer 也是信號方案,可以觀察進程信息中信號捕獲。SIGEV_THREAD 模式下,會啟動一個輔助線程,然后也是監聽到 SIGTIMER 信號,再做后續處理,源碼可見 https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/timer_routines.c.html。

稍微總結一下,POSIX Timer 的方案,相比較之前已經完善了很多,不過還有一些缺點。

  1. 封裝處理較為麻煩
  2. 必須依賴 librt

使用該方案的開源項目有 gperftools,核心的代碼位置在 https://github.com/gperftools/gperftools/blob/master/src/profile-handler.cc#L282。
封裝方式和上文中的例子差不多,只是模式不一樣,這里就不詳細講解了。

多路復用

多路復用本身是為了解決服務器針對多連接時的阻塞問題,不過 select/poll/epoll 都提供了超時時間,而這一特性可以讓我們使用到定時器中。
以 boost 的 timer 為例,看如下代碼

代碼很好理解,我們看一下 boost 是如何實現一個同步的 timer.wait 能力的,順著 deadline_timer_service 可以找到最后源碼位置在 https://github.com/boostorg/asio/blob/develop/include/boost/asio/detail/impl/socket_ops.ipp#L2162,簡單到無需多余講解。
BTW: 并且這里只用超時能力,不用擔心 select 本身在多路復用中的性能問題。

boost 中的異步定時器,也是采用了多路復用的方案,使用的是 epoll,其中用到了 timer_fd,先簡單的說一下 timer_fd。
timer_fd 是 linux2.6.25 后增加的 api,算是官方形式將定時能力和 IO 事件結合了起來。
異步定時器相比較同步復雜很多,所以我們通過分析 boost 中異步定時器的源碼來詳細展開下。
先畫個圖:
boost 異步定時任務流程
然后我們依次看一下。

  1. 將 timer_fd 綁定到 epoll_fd 上
    epoll 使用一個文件描述符 (epoll_fd) 管理多個描述符 (例如這里的 timer_fd),這樣在用戶空間和內核空間的 copy 只需一次。
    切記:這里 timer_fd 也需要進行復用,如果每次一個定時任務,都用一個新的 timer_fd,會有嚴重的性能浪費。

    整體較好理解,幾個重要的點增加了注釋

  2. 添加任務到 timer_queue

    enqueue_timer 里面大部分代碼我省略掉了,也就是在維護一個小根堆,讓最近的定時任務在前面,這樣可以方便第三步啟動和更新 timerfd。
    BTW: 這里小根堆并不是像我們之前 demo 用了 priority_queue 方式,而是每次 push_back 會去 swap 修改 vector。

  3. 啟動/更新 timerfd
    結合上一節的代碼,當 enqueue_timer 返回 true 的時候,就會去更新/啟動定時器。

    注意:如果不支持 timerfd,則會直接調用 epoll_ctl。

  4. 啟動 epoll_wait

  5. 收到 IO 事件,從 timer_queue 中判斷過期任務

這兩步的代碼位置太過相近,就放一起來說了。

重點的就是有?標志的代碼。
timer_queues 里面發現的過期事件會添加到 op_queue 里面去,如下:

op_queue 會在 scheduler.ipp 內進行執行。

以上就是 boost 中的異步定時器執行分解,感興趣的同學也可以自己下源碼來學習。
BTW: libevent 中定時任務做法與 boost 基本一致,chromium 底層的 message_pump 也有使用 libevent。

總結

我們了解到需要實現一個定時器/定時任務,重點需要兩塊:

  1. 存放執行回調的地方
    大部分選擇是小根堆的方案,簡單方便;也有時間輪的方案。
  2. 調用操作系統提供的定時能力
    我們分析了「信號」「POSIX Timer」「多路復用」,信號 pass,后二者中更推薦多路復用一些。
    分析了 boost asio 的源碼,學習了多路復用能力用在定時方面的解決辦法。

如果你還想了解的更多,可以學習 libevent、libco、chromium 中定時器方面采取的方案。
歡迎一起討論研究~

原創文章轉載請注明:

轉載自AlloyTeam:http://www.ecomenagepro.com/2021/03/15389/

發表評論