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