sleep()到底睡多久,你知道嗎?
丁鐸
2014年畢業加入騰訊,對終端的性能測試有豐富的經驗,《Android移動性能實戰》作者之一,現在從事后臺的性能測試。
***本文共2008個字,閱讀需要5分鐘,本文經授權轉自騰訊藍鯨(微信號:Tencent_lanjing)
1. 背景
最近負責一個很簡單的需求:在服務器上起一個后臺進程,每隔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(){ m_iInterval = INTERVAL;while(m_bFlag){updateData();Sleep(m_iInterval*1000);}m_acq=1;return SUCCEED;}
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): self._wakeup=Event()
官網對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看起來是個不錯的任務調度庫。
“Linuxy云計算25期開班倒計時5天”
