之前介紹過的所有阻塞調用,將會阻塞一段不確定的時間,將線程掛起直到等待的事件發(fā)生。在很多情況下,這樣的方式很不錯,但是在其他一些情況下,你就需要限制一下線程等待的時間了。這允許你發(fā)送一些類似“我還存活”的信息,無論是對交互式用戶,或是其他進程,亦或當用戶放棄等待,你可以按下“取消”鍵直接終止等待。
介紹兩種可能是你希望指定的超時方式:一種是“時延”的超時方式,另一種是“絕對”超時方式。第一種方式,需要指定一段時間(例如,30毫秒);第二種方式,就是指定一個時間點(例如,協(xié)調世界時[UTC]17:30:15.045987023,2011年11月30日)。多數(shù)等待函數(shù)提供變量,對兩種超時方式進行處理。處理持續(xù)時間的變量以“_for”作為后綴,處理絕對時間的變量以"_until"作為后綴。
所以,當std::condition_variable的兩個成員函數(shù)wait_for()和wait_until()成員函數(shù)分別有兩個負載,這兩個負載都與wait()成員函數(shù)的負載相關——其中一個負載只是等待信號觸發(fā),或時間超期,亦或是一個虛假的喚醒,并且醒來時,會檢查鎖提供的謂詞,并且只有在檢查為true時才會返回(這時條件變量的條件達成),或直接而超時。
在我們觀察使用超時函數(shù)的細節(jié)前,讓我們來檢查一下時間在C++中指定的方式,就從時鐘開始吧!
對于C++標準庫來說,時鐘就是時間信息源。特別是,時鐘是一個類,提供了四種不同的信息:
現(xiàn)在時間
時間類型
時鐘節(jié)拍
時鐘的當前時間可以通過調用靜態(tài)成員函數(shù)now()從時鐘類中獲取;例如,std::chrono::system_clock::now()是將返回系統(tǒng)時鐘的當前時間。特定的時間點類型可以通過time_point的數(shù)據(jù)typedef成員來指定,所以some_clock::now()的類型就是some_clock::time_point。
時鐘節(jié)拍被指定為1/x(x在不同硬件上有不同的值)秒,這是由時間周期所決定——一個時鐘一秒有25個節(jié)拍,因此一個周期為std::ratio<1, 25>,當一個時鐘的時鐘節(jié)拍每2.5秒一次,周期就可以表示為std::ratio<5, 2>。當時鐘節(jié)拍直到運行時都無法知曉,可以使用一個給定的應用程序運行多次,周期可以用執(zhí)行的平均時間求出,其中最短的時間可能就是時鐘節(jié)拍,或者是直接寫在手冊當中。這就不保證在給定應用中觀察到的節(jié)拍周期與指定的時鐘周期相匹配。
當時鐘節(jié)拍均勻分布(無論是否與周期匹配),并且不可調整,這種時鐘就稱為穩(wěn)定時鐘。當is_steady靜態(tài)數(shù)據(jù)成員為true時,表明這個時鐘就是穩(wěn)定的,否則,就是不穩(wěn)定的。通常情況下,std::chrono::system_clock是不穩(wěn)定的,因為時鐘是可調的,即是這種是完全自動適應本地賬戶的調節(jié)。這種調節(jié)可能造成的是,首次調用now()返回的時間要早于上次調用now()所返回的時間,這就違反了節(jié)拍頻率的均勻分布。穩(wěn)定鬧鐘對于超時的計算很重要,所以C++標準庫提供一個穩(wěn)定時鐘std::chrono::steady_clock。C++標準庫提供的其他時鐘可表示為std::chrono::system_clock(在上面已經提到過),它代表了系統(tǒng)時鐘的“實際時間”,并且提供了函數(shù)可將時間點轉化為time_t類型的值;std::chrono::high_resolution_clock 可能是標準庫中提供的具有最小節(jié)拍周期(因此具有最高的精度[分辨率])的時鐘。它實際上是typedef的另一種時鐘,這些時鐘和其他與時間相關的工具,都被定義在
我們馬上來看一下時間點是如何表示的,但在這之前,我們先看一下持續(xù)時間是怎么表示的。
時延是時間部分最簡單的;std::chrono::duration<>函數(shù)模板能夠對時延進行處理(線程庫使用到的所有C++時間處理工具,都在std::chrono命名空間內)。第一個模板參數(shù)是一個類型表示(比如,int,long或double),第二個模板參數(shù)是制定部分,表示每一個單元所用秒數(shù)。例如,當幾分鐘的時間要存在short類型中時,可以寫成std::chrono::duration<short, std::ratio<60, 1>>,因為60秒是才是1分鐘,所以第二個參數(shù)寫成std::ratio<60, 1>。另一方面,當需要將毫秒級計數(shù)存在double類型中時,可以寫成std::chrono::duration<double, std::ratio<1, 1000>>,因為1秒等于1000毫秒。
標準庫在std::chrono命名空間內,為延時變量提供一系列預定義類型:nanoseconds[納秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[時]。比如,你要在一個合適的單元表示一段超過500年的時延,預定義類型可充分利用了大整型,來表示所要表示的時間類型。當然,這里也定義了一些國際單位制(SI, [法]le Système international d'unités)分數(shù),可從std::atto(10^(-18))到std::exa(10^(18))(題外話:當你的平臺支持128位整型);也可以指定自定義時延類型,例如,std::duration<double, std::centi>,就可以使用一個double類型的變量表示1/100。
當不要求截斷值的情況下(時轉換成秒是沒問題,但是秒轉換成時就不行)時延的轉換是隱式的。顯示轉換可以由std::chrono::duration_cast<>來完成。
std::chrono::milliseconds ms(54802);
std::chrono::seconds s=
std::chrono::duration_cast<std::chrono::seconds>(ms);
這里的結果就是截斷的,而不是進行了舍入,所以s最后的值將為54。
延遲支持計算,所以你能夠對兩個時延變量進行加減,或者是對一個時延變量乘除一個常數(shù)(模板的第一個參數(shù))來獲得一個新延遲變量。例如,5*seconds(1)與seconds(5)或minutes(1)-seconds(55)一樣。在時延中可以通過count()成員函數(shù)獲得單位時間的數(shù)量。例如,std::chrono::milliseconds(1234).count()就是1234。
基于時延的等待可由std::chrono::duration<>來完成。例如,你等待一個“期望”狀態(tài)變?yōu)榫途w已經35毫秒:
std::future<int> f=std::async(some_task);
if(f.wait_for(std::chrono::milliseconds(35))==std::future_status::ready)
do_something_with(f.get());
等待函數(shù)會返回一個狀態(tài)值,來表示等待是超時,還是繼續(xù)等待。在這種情況下,你可以等待一個“期望”,所以當函數(shù)等待超時時,會返回std::future_status::timeout;當“期望”狀態(tài)改變,函數(shù)會返回std::future_status::ready;當“期望”的任務延遲了,函數(shù)會返回std::future_status::deferred。基于時延的等待是使用內部庫提供的穩(wěn)定時鐘,來進行計時的;所以,即使系統(tǒng)時鐘在等待時被調整(向前或向后),35毫秒的時延在這里意味著,的確耗時35毫秒。當然,難以預料的系統(tǒng)調度和不同操作系統(tǒng)的時鐘精度都意味著:在線程中,從調用到返回的實際時間可能要比35毫秒長。
時延中沒有特別好的辦法來處理以上情況,所以我們暫且停下對時延的討論。現(xiàn)在,我們就要來看看“時間點”是怎么樣工作的。
時鐘的時間點可以用std::chrono::time_point<>的類型模板實例來表示,實例的第一個參數(shù)用來指定所要使用的時鐘,第二個函數(shù)參數(shù)用來表示時間的計量單位(特化的std::chrono::duration<>)。一個時間點的值就是時間的長度(在指定時間的倍數(shù)內),例如,指定“unix時間戳”(epoch)為一個時間點。時間戳是時鐘的一個基本屬性,但是不可以直接查詢,或在C++標準中已經指定。通常,unix時間戳表示1970年1月1日 00:00,即計算機啟動應用程序時。時鐘可能共享一個時間戳,或具有獨立的時間戳。當兩個時鐘共享一個時間戳時,其中一個time_point類型可以與另一個時鐘類型中的time_point相關聯(lián)。這里,雖然你無法知道unix時間戳是什么,但是你可以通過對指定time_point類型使用time_since_epoch()來獲取時間戳。這個成員函數(shù)會返回一個時延值,這個時延值是指定時間點到時鐘的unix時間戳鎖用時。
例如,你可能指定了一個時間點std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>。這就與系統(tǒng)時鐘有關,且實際中的一分鐘與系統(tǒng)時鐘精度應該不相同(通常差幾秒)。
你可以通過std::chrono::time_point<>實例來加/減時延,來獲得一個新的時間點,所以std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500)將得到500納秒后的時間。當你知道一塊代碼的最大時延時,這對于計算絕對時間的超時是一個好消息,當?shù)却龝r間內,等待函數(shù)進行多次調用;或,非等待函數(shù)且占用了等待函數(shù)時延中的時間。
你也可以減去一個時間點(二者需要共享同一個時鐘)。結果是兩個時間點的時間差。這對于代碼塊的計時是很有用的,例如:
auto start=std::chrono::high_resolution_clock::now();
do_something();
auto stop=std::chrono::high_resolution_clock::now();
std::cout<<”do_something() took “
<<std::chrono::duration<double,std::chrono::seconds>(stop-start).count()
<<” seconds”<<std::endl;
std::chrono::time_point<>實例的時鐘參數(shù)可不僅是能夠指定unix時間戳的。當你想一個等待函數(shù)(絕對時間超時的方式)傳遞時間點時,時間點的時鐘參數(shù)就被用來測量時間。當時鐘變更時,會產生嚴重的后果,因為等待軌跡隨著時鐘的改變而改變,并且知道調用時鐘的now()成員函數(shù)時,才能返回一個超過超時時間的值。當時鐘向前調整,這就有可能減小等待時間的總長度(與穩(wěn)定時鐘的測量相比);當時鐘向后調整,就有可能增加等待時間的總長度。
如你期望的那樣,后綴為_unitl的(等待函數(shù)的)變量會使用時間點。通常是使用某些時鐘的::now()(程序中一個固定的時間點)作為偏移,雖然時間點與系統(tǒng)時鐘有關,可以使用std::chrono::system_clock::to_time_point() 靜態(tài)成員函數(shù),在用戶可視時間點上進行調度操作。例如,當你有一個對多等待500毫秒的,且與條件變量相關的事件,你可以參考如下代碼:
清單4.11 等待一個條件變量——有超時功能
#include <condition_variable>
#include <mutex>
#include <chrono>
std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop()
{
auto const timeout= std::chrono::steady_clock::now()+
std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while(!done)
{
if(cv.wait_until(lk,timeout)==std::cv_status::timeout)
break;
}
return done;
}
這種方式是我們推薦的,當你沒有什么事情可以等待時,可在一定時限中等待條件變量。在這種方式中,循環(huán)的整體長度是有限的。如你在4.1.1節(jié)中所見,當使用條件變量(且無事可待)時,你就需要使用循環(huán),這是為了處理假喚醒。當你在循環(huán)中使用wait_for()時,你可能在等待了足夠長的時間后結束等待(在假喚醒之前),且下一次等待又開始了。這可能重復很多次,使得等待時間無邊無際。
到此,有關時間點超時的基本知識你已經了解了。現(xiàn)在,讓我們來了解一下如何在函數(shù)中使用超時。
使用超時的最簡單方式就是,對一個特定線程添加一個延遲處理;當這個線程無所事事時,就不會占用可供其他線程處理的時間。你在4.1節(jié)中看過一個例子,你循環(huán)檢查“done”標志。兩個處理函數(shù)分別是std::this_thread::sleep_for()和std::this_thread::sleep_until()。他們的工作就像一個簡單的鬧鐘:當線程因為指定時延而進入睡眠時,可使用sleep_for()喚醒;或因指定時間點睡眠的,可使用sleep_until喚醒。sleep_for()的使用如同在4.1節(jié)中的例子,有些事必須在指定時間范圍內完成,所以耗時在這里就很重要。另一方面,sleep_until()允許在某個特定時間點將調度線程喚醒。這有可能在晚間備份,或在早上6:00打印工資條時使用,亦或掛起線程直到下一幀刷新時進行視頻播放。
當然,休眠只是超時處理的一種形式;你已經看到了,超時可以配合條件變量和“期望”一起使用。超時甚至可以在嘗試獲取一個互斥鎖時(當互斥量支持超時時)使用。std::mutex和std::recursive_mutex都不支持超時鎖,但是std::timed_mutex和std::recursive_timed_mutex支持。這兩種類型也有try_lock_for()和try_lock_until()成員函數(shù),可以在一段時期內嘗試,或在指定時間點前獲取互斥鎖。表4.1展示了C++標準庫中支持超時的函數(shù)。參數(shù)列表為“延時”(duration)必須是std::duration<>的實例,并且列出為時間點(time_point)必須是std::time_point<>的實例。
表4.1 可接受超時的函數(shù)
| 類型/命名空間 | 函數(shù) | 返回值 |
| std::this_thread[namespace] | sleep_for(duration) | N/A |
| sleep_until(time_point) | ||
| std::condition_variable 或 std::condition_variable_any | wait_for(lock, duration) | std::cv_status::time_out 或 std::cv_status::no_timeout |
| wait_until(lock, time_point) | ||
| wait_for(lock, duration, predicate) | bool —— 當喚醒時,返回謂詞的結果 | |
| wait_until(lock, duration, predicate) | ||
| std::timed_mutex 或 std::recursive_timed_mutex | try_lock_for(duration) | bool —— 獲取鎖時返回true,否則返回fasle |
| try_lock_until(time_point) | ||
| std::unique_lock<TimedLockable> | unique_lock(lockable, duration) | N/A —— 對新構建的對象調用owns_lock(); |
| unique_lock(lockable, time_point) | 當獲取鎖時返回true,否則返回false | |
| try_lock_for(duration) | bool —— 當獲取鎖時返回true,否則返回false | |
| try_lock_until(time_point) | ||
| std::future<ValueType>或std::shared_future<ValueType> | wait_for(duration) | 當?shù)却瑫r,返回std::future_status::timeout |
| wait_until(time_point) | 當“期望”準備就緒時,返回std::future_status::ready | |
| 當“期望”持有一個為啟動的延遲函數(shù),返回std::future_status::deferred |
現(xiàn)在,我們討論的機制有:條件變量、“期望”、“承諾”還有打包的任務。是時候從更高的角度去看待這些機制,怎么樣使用這些機制,簡化線程的同步操作。