sleep()到底睡多久,你知道嗎?
最近負責一個很簡單的需求:在伺服器上起一個後台進程,每隔10秒鐘上報一下CPU、內存等信息。就是這麼簡單的需求,發生了一個有趣的問題。
通過資料庫查看上報的數據,發現Windows伺服器在5月24號14:59到15:12之間,少上報了一個數據,少上報的數據會用null填充。但是看子機上的日誌,這段時間均是按照預設的間隔成功上報,那問題出在哪兒呢?
開發一時也是一臉茫然,建議把測試時間調長,看是否能找到規律。好吧,那就把測試時間改到19個小時,這下還真的發現了一點規律。
上圖第一列是這段時間上報的數據點序列,即第95個點,第419個點,第二列是上報的信息,把所有的null過濾出來,看到相鄰行的序號相差都在320~330之間,換算成時間,就是大概55分鐘會少上報一個數據。
2. 原因排查
雖然找到了掉點的規律,但是從子機日誌看都是上報成功的,是因為子機在這段時間就少採集了一個點嗎?如果是這樣的話,那麼每次採樣周期應該是超過10s的。
下面是信息採集上報的主循環代碼,這裡m_iInterval為5,也就是在每個採樣周期內,這個循環會執行兩次,然後上報這兩次中最大的值。
int nmISensor::execute(){
2.1 猜測1:updateData()有耗時,導致整個循環周期的時間大於預期
從上面的代碼可以看出,每個上報周期,代碼的執行邏輯如下示意圖,我們第一反應是updateData()的執行肯定也會耗時,那麼會導致整個採樣周期大於10s。一段時間後,就會少上報一個數據,幸福好像來的太突然。
為了驗證這個猜想,我們統計了一下updataData()的耗時,統計的結果看updateData()耗時都是0,也就是updateData()基本上是不耗時的,事實和我們預想的並不一樣。
2.2 猜想2:Sleep()有誤差
排除了updeData()的原因,現在只能把目光聚焦在Sleep函數上,難道是Windows的Sleep函數實際休眠的時間和預期有差異?為了驗證這個猜想,我們又在日誌中打出了Sleep實際執行的耗時和預期之間的差異。
這次好像看到了希望,從輸出的日誌看,Sleep最終休眠的時間會比預期多15ms,這樣以來,每個上報周期就會多30ms,也就是在55分鐘內可以上報330個點,現在只能上報329個點。
那麼問題來了,為什麼在Windows上Sleep()會比預期的多15ms呢?
我們知道Windows操作系統基於時間片來進行任務調度的,Windows內核的時鐘頻率為64HZ,也就是每個時間片是15.625同時Windows也是非實時操作系統。對於非實時操作系統來說,低優先順序的任務只有在子機的時間片結束或者主動掛起時,高優先順序的任務才能被調度。下圖直觀地展示了兩類操作系統的區別。
MSDN 上對Sleep()的說明:Sleep()需要依賴內核的時間片,如果休眠時間在1~2時間片之間,那麼最終等待的時間會是1個或者2個時間片,也就是Sleep()會有0-15.625(1個時間片)的誤差,那麼到這裡我們的問題也就弄清楚了。
3. 解決方案
3.1 官方方案
微軟官方針對Sleep耗時不精確的問題,也給出相應的解決方案:
調用timeGetDevCaps獲取時鐘定時器能支持的最小粒度
在定時開始之前調用timeBeginPeriod,這樣會把時鐘定時器設置為最小的粒度
在定時結束之後調用 timeEndPeriod,恢復時鐘定時器的粒度
同時,官方文檔也指出timeBeginPeriod會對系統時鐘、系統耗電和任務調度有影響,也就是timeBeginPeriod雖好,當不能濫用。
3.2 開發的方案
開發最後沒有採用官方給的方案, 畢竟頻繁調用timeBeginPeriod,帶來的影響很難預估。而是採用了比較巧妙的方法:本次等待時長會減去上次多等的時間,即如果上次多等了15ms,那麼下次只用等4895ms就可以了,這樣可以保證每次循環周期是10s。
dwStart = GetTickCount();Sleep(dwInterval);dwDiff = GetTickCount() - dwStart - dwInterval;dwInterval = m_iInterval*1000;if (((long)dwDiff > 0) && (dwDiff < dwInterval)){dwInterval -= dwDiff;}
寫到這裡,問題已經解決,這時又有個疑惑湧上心頭,Linux伺服器上有同樣的上報功能,為什麼Linux子機沒有這個問題呢?難道Linux對應的開發是大嬸,已瞭然這一切?
4. Linux系統上sleep()是怎樣的呢?
找到了Linux上對應的代碼,原來這個開發哥並沒有像Windows的開發哥那樣自己去寫一個定時的任務調度,而是用了一個開源的任務調度庫APScheduler,才免遭遇難。看來這裡的奧秘都在這個開源庫中,接著就去看看APScheduler是怎樣做任務調度的。 APScheduler主循環的代碼如下,紅框圈出了一行關鍵的代碼,這行代碼的意思是:本次任務執行完成之後,在下次任務開始前需要等待wait_sechonds的時間。
而self._wakeup是一個Event的對象,而Event正是Python系統庫threading 中定義的。而Event常用來做多線程的同步。
def __init__(self, gconfig={}, **options):
官網對Event.wait()的解釋:調用wait()之後,線程會一直阻塞,直到內部的flag設置為true,或者超時。在沒有別的線程設置internal flag時,wait()就可以起到一個定時器的作用。
wait([timeout])Block until the internal flag is true. If the internal flag is true on entry, return immediately. Otherwise, block until another thread calls set() to set the flag to true, or until the optional timeout occurs.
那麼問題又來了,Event.wait()如果用作定時器,誤差是多少呢?寫個demp驗證一下。
從測試數據看,Event.wait()取100次的平均偏差為0.1ms,而time.sleep()的平均偏差為7.65ms,看起來Event.wait()精度更高。
這裡再回到我們第一個猜想,其實我們的猜想是合情合理的,如果updateData()的耗時較長,整個循環周期必定會超過預定的值,所以這裡的實現並不嚴謹,而 APScheduler則是通過起一個新的線程去執行任務,並不會阻塞循環周期,可以看到APScheduler在這裡處理的還是很合理的。
5. 結論
啰嗦了這麼多,總結一下上面的內容:
sleep()在Windows和Linux系統上和預設值都會存在一個偏差,偏差最大為1個時間片的時間;
Event.wait()用來做定時器精度會更高,可以達到0.1ms;
APScheduler看起來是個不錯的任務調度庫。
TAG:PHP愛好者 |
※Get Some Sleep 你不知道的睡眠的好處
※「asleep」和「sleepy」傻傻分不清楚?看了你就會了
※睡覺失眠?你或許該考慮下Bose Sleepbuds
※Bose SleepBuds睡眠耳塞 花錢就能買到的好睡眠
※沉睡者 Sleepers
※when do you sleep?-健康睡眠時間
※Nosleep:束縛(上)
※Bose發布遮噪耳塞Sleepbuds,只為讓你睡個好覺
※別讓幼犬哭泣 Let sleeping dogs lie…but never let puppydogs cry!
※從睡眠監測的二三事,說到久違的諾基亞Nokia Sleep
※Nosleep:束縛(完)
※Bose noise-masking sleepbuds? 遮噪睡眠耳塞實測報告
※Nosleep:廢墟餘響
※以「噪」止噪,是Bose Noise-Masking Sleepbuds的安睡秘訣
※實測BOSE睡眠耳塞Sleepbuds是否真能阻隔雜訊?
※nosleep:險惡勿近
※Nosleep:束縛(中)
※macOS:設定 Mac 的睡眠和喚醒時間Set sleep and wake times for your Mac
※Sleep+3應用更新,讓Apple Watch睡眠追蹤更為簡單
※Sleeping Dogs:律動在前,這回遊戲可以被拋到腦後