鍍金池/ 教程/ C/ 4.3 限定等待時(shí)間
3.4 本章總結(jié)
6.3 基于鎖設(shè)計(jì)更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)
6.1 為并發(fā)設(shè)計(jì)的意義何在?
5.2 <code>C++</code>中的原子操作和原子類型
A.7 自動(dòng)推導(dǎo)變量類型
2.1 線程管理的基礎(chǔ)
8.5 在實(shí)踐中設(shè)計(jì)并發(fā)代碼
2.4 運(yùn)行時(shí)決定線程數(shù)量
2.2 向線程函數(shù)傳遞參數(shù)
第4章 同步并發(fā)操作
2.3 轉(zhuǎn)移線程所有權(quán)
8.3 為多線程性能設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)
6.4 本章總結(jié)
7.3 對于設(shè)計(jì)無鎖數(shù)據(jù)結(jié)構(gòu)的指導(dǎo)建議
關(guān)于這本書
A.1 右值引用
2.6 本章總結(jié)
D.2 &lt;condition_variable&gt;頭文件
A.6 變參模板
6.2 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)
4.5 本章總結(jié)
A.9 本章總結(jié)
前言
第10章 多線程程序的測試和調(diào)試
5.4 本章總結(jié)
第9章 高級線程管理
5.1 內(nèi)存模型基礎(chǔ)
2.5 識別線程
第1章 你好,C++的并發(fā)世界!
1.2 為什么使用并發(fā)?
A.5 Lambda函數(shù)
第2章 線程管理
4.3 限定等待時(shí)間
D.3 &lt;atomic&gt;頭文件
10.2 定位并發(fā)錯(cuò)誤的技術(shù)
附錄B 并發(fā)庫的簡單比較
5.3 同步操作和強(qiáng)制排序
A.8 線程本地變量
第8章 并發(fā)代碼設(shè)計(jì)
3.3 保護(hù)共享數(shù)據(jù)的替代設(shè)施
附錄D C++線程庫參考
第7章 無鎖并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
D.7 &lt;thread&gt;頭文件
D.1 &lt;chrono&gt;頭文件
4.1 等待一個(gè)事件或其他條件
A.3 默認(rèn)函數(shù)
附錄A 對<code>C++</code>11語言特性的簡要介紹
第6章 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
封面圖片介紹
7.2 無鎖數(shù)據(jù)結(jié)構(gòu)的例子
8.6 本章總結(jié)
8.1 線程間劃分工作的技術(shù)
4.2 使用期望等待一次性事件
8.4 設(shè)計(jì)并發(fā)代碼的注意事項(xiàng)
D.5 &lt;mutex&gt;頭文件
3.1 共享數(shù)據(jù)帶來的問題
資源
9.3 本章總結(jié)
10.3 本章總結(jié)
10.1 與并發(fā)相關(guān)的錯(cuò)誤類型
D.4 &lt;future&gt;頭文件
3.2 使用互斥量保護(hù)共享數(shù)據(jù)
9.1 線程池
1.1 何謂并發(fā)
9.2 中斷線程
4.4 使用同步操作簡化代碼
A.2 刪除函數(shù)
1.3 C++中的并發(fā)和多線程
1.4 開始入門
第5章 C++內(nèi)存模型和原子類型操作
消息傳遞框架與完整的ATM示例
8.2 影響并發(fā)代碼性能的因素
7.1 定義和意義
D.6 &lt;ratio&gt;頭文件
A.4 常量表達(dá)式函數(shù)
7.4 本章總結(jié)
1.5 本章總結(jié)
第3章 線程間共享數(shù)據(jù)

4.3 限定等待時(shí)間

之前介紹過的所有阻塞調(diào)用,將會(huì)阻塞一段不確定的時(shí)間,將線程掛起直到等待的事件發(fā)生。在很多情況下,這樣的方式很不錯(cuò),但是在其他一些情況下,你就需要限制一下線程等待的時(shí)間了。這允許你發(fā)送一些類似“我還存活”的信息,無論是對交互式用戶,或是其他進(jìn)程,亦或當(dāng)用戶放棄等待,你可以按下“取消”鍵直接終止等待。

介紹兩種可能是你希望指定的超時(shí)方式:一種是“時(shí)延”的超時(shí)方式,另一種是“絕對”超時(shí)方式。第一種方式,需要指定一段時(shí)間(例如,30毫秒);第二種方式,就是指定一個(gè)時(shí)間點(diǎn)(例如,協(xié)調(diào)世界時(shí)[UTC]17:30:15.045987023,2011年11月30日)。多數(shù)等待函數(shù)提供變量,對兩種超時(shí)方式進(jìn)行處理。處理持續(xù)時(shí)間的變量以“_for”作為后綴,處理絕對時(shí)間的變量以"_until"作為后綴。

所以,當(dāng)std::condition_variable的兩個(gè)成員函數(shù)wait_for()和wait_until()成員函數(shù)分別有兩個(gè)負(fù)載,這兩個(gè)負(fù)載都與wait()成員函數(shù)的負(fù)載相關(guān)——其中一個(gè)負(fù)載只是等待信號觸發(fā),或時(shí)間超期,亦或是一個(gè)虛假的喚醒,并且醒來時(shí),會(huì)檢查鎖提供的謂詞,并且只有在檢查為true時(shí)才會(huì)返回(這時(shí)條件變量的條件達(dá)成),或直接而超時(shí)。

在我們觀察使用超時(shí)函數(shù)的細(xì)節(jié)前,讓我們來檢查一下時(shí)間在C++中指定的方式,就從時(shí)鐘開始吧!

4.3.1 時(shí)鐘

對于C++標(biāo)準(zhǔn)庫來說,時(shí)鐘就是時(shí)間信息源。特別是,時(shí)鐘是一個(gè)類,提供了四種不同的信息:

  • 現(xiàn)在時(shí)間

  • 時(shí)間類型

  • 時(shí)鐘節(jié)拍

  • 通過時(shí)鐘節(jié)拍的分布,判斷時(shí)鐘是否穩(wěn)定

時(shí)鐘的當(dāng)前時(shí)間可以通過調(diào)用靜態(tài)成員函數(shù)now()從時(shí)鐘類中獲?。焕?,std::chrono::system_clock::now()是將返回系統(tǒng)時(shí)鐘的當(dāng)前時(shí)間。特定的時(shí)間點(diǎn)類型可以通過time_point的數(shù)據(jù)typedef成員來指定,所以some_clock::now()的類型就是some_clock::time_point。

時(shí)鐘節(jié)拍被指定為1/x(x在不同硬件上有不同的值)秒,這是由時(shí)間周期所決定——一個(gè)時(shí)鐘一秒有25個(gè)節(jié)拍,因此一個(gè)周期為std::ratio<1, 25>,當(dāng)一個(gè)時(shí)鐘的時(shí)鐘節(jié)拍每2.5秒一次,周期就可以表示為std::ratio<5, 2>。當(dāng)時(shí)鐘節(jié)拍直到運(yùn)行時(shí)都無法知曉,可以使用一個(gè)給定的應(yīng)用程序運(yùn)行多次,周期可以用執(zhí)行的平均時(shí)間求出,其中最短的時(shí)間可能就是時(shí)鐘節(jié)拍,或者是直接寫在手冊當(dāng)中。這就不保證在給定應(yīng)用中觀察到的節(jié)拍周期與指定的時(shí)鐘周期相匹配。

當(dāng)時(shí)鐘節(jié)拍均勻分布(無論是否與周期匹配),并且不可調(diào)整,這種時(shí)鐘就稱為穩(wěn)定時(shí)鐘。當(dāng)is_steady靜態(tài)數(shù)據(jù)成員為true時(shí),表明這個(gè)時(shí)鐘就是穩(wěn)定的,否則,就是不穩(wěn)定的。通常情況下,std::chrono::system_clock是不穩(wěn)定的,因?yàn)闀r(shí)鐘是可調(diào)的,即是這種是完全自動(dòng)適應(yīng)本地賬戶的調(diào)節(jié)。這種調(diào)節(jié)可能造成的是,首次調(diào)用now()返回的時(shí)間要早于上次調(diào)用now()所返回的時(shí)間,這就違反了節(jié)拍頻率的均勻分布。穩(wěn)定鬧鐘對于超時(shí)的計(jì)算很重要,所以C++標(biāo)準(zhǔn)庫提供一個(gè)穩(wěn)定時(shí)鐘std::chrono::steady_clock。C++標(biāo)準(zhǔn)庫提供的其他時(shí)鐘可表示為std::chrono::system_clock(在上面已經(jīng)提到過),它代表了系統(tǒng)時(shí)鐘的“實(shí)際時(shí)間”,并且提供了函數(shù)可將時(shí)間點(diǎn)轉(zhuǎn)化為time_t類型的值;std::chrono::high_resolution_clock 可能是標(biāo)準(zhǔn)庫中提供的具有最小節(jié)拍周期(因此具有最高的精度[分辨率])的時(shí)鐘。它實(shí)際上是typedef的另一種時(shí)鐘,這些時(shí)鐘和其他與時(shí)間相關(guān)的工具,都被定義在庫頭文件中。

我們馬上來看一下時(shí)間點(diǎn)是如何表示的,但在這之前,我們先看一下持續(xù)時(shí)間是怎么表示的。

4.3.2 時(shí)延

時(shí)延是時(shí)間部分最簡單的;std::chrono::duration<>函數(shù)模板能夠?qū)r(shí)延進(jìn)行處理(線程庫使用到的所有C++時(shí)間處理工具,都在std::chrono命名空間內(nèi))。第一個(gè)模板參數(shù)是一個(gè)類型表示(比如,int,long或double),第二個(gè)模板參數(shù)是制定部分,表示每一個(gè)單元所用秒數(shù)。例如,當(dāng)幾分鐘的時(shí)間要存在short類型中時(shí),可以寫成std::chrono::duration<short, std::ratio<60, 1>>,因?yàn)?0秒是才是1分鐘,所以第二個(gè)參數(shù)寫成std::ratio<60, 1>。另一方面,當(dāng)需要將毫秒級計(jì)數(shù)存在double類型中時(shí),可以寫成std::chrono::duration<double, std::ratio<1, 1000>>,因?yàn)?秒等于1000毫秒。

標(biāo)準(zhǔn)庫在std::chrono命名空間內(nèi),為延時(shí)變量提供一系列預(yù)定義類型:nanoseconds[納秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[時(shí)]。比如,你要在一個(gè)合適的單元表示一段超過500年的時(shí)延,預(yù)定義類型可充分利用了大整型,來表示所要表示的時(shí)間類型。當(dāng)然,這里也定義了一些國際單位制(SI, [法]le Système international d'unités)分?jǐn)?shù),可從std::atto(10^(-18))std::exa(10^(18))(題外話:當(dāng)你的平臺支持128位整型);也可以指定自定義時(shí)延類型,例如,std::duration<double, std::centi>,就可以使用一個(gè)double類型的變量表示1/100。

當(dāng)不要求截?cái)嘀档那闆r下(時(shí)轉(zhuǎn)換成秒是沒問題,但是秒轉(zhuǎn)換成時(shí)就不行)時(shí)延的轉(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é)果就是截?cái)嗟模皇沁M(jìn)行了舍入,所以s最后的值將為54。

延遲支持計(jì)算,所以你能夠?qū)蓚€(gè)時(shí)延變量進(jìn)行加減,或者是對一個(gè)時(shí)延變量乘除一個(gè)常數(shù)(模板的第一個(gè)參數(shù))來獲得一個(gè)新延遲變量。例如,5*seconds(1)與seconds(5)或minutes(1)-seconds(55)一樣。在時(shí)延中可以通過count()成員函數(shù)獲得單位時(shí)間的數(shù)量。例如,std::chrono::milliseconds(1234).count()就是1234。

基于時(shí)延的等待可由std::chrono::duration<>來完成。例如,你等待一個(gè)“期望”狀態(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ù)會(huì)返回一個(gè)狀態(tài)值,來表示等待是超時(shí),還是繼續(xù)等待。在這種情況下,你可以等待一個(gè)“期望”,所以當(dāng)函數(shù)等待超時(shí)時(shí),會(huì)返回std::future_status::timeout;當(dāng)“期望”狀態(tài)改變,函數(shù)會(huì)返回std::future_status::ready;當(dāng)“期望”的任務(wù)延遲了,函數(shù)會(huì)返回std::future_status::deferred。基于時(shí)延的等待是使用內(nèi)部庫提供的穩(wěn)定時(shí)鐘,來進(jìn)行計(jì)時(shí)的;所以,即使系統(tǒng)時(shí)鐘在等待時(shí)被調(diào)整(向前或向后),35毫秒的時(shí)延在這里意味著,的確耗時(shí)35毫秒。當(dāng)然,難以預(yù)料的系統(tǒng)調(diào)度和不同操作系統(tǒng)的時(shí)鐘精度都意味著:在線程中,從調(diào)用到返回的實(shí)際時(shí)間可能要比35毫秒長。

時(shí)延中沒有特別好的辦法來處理以上情況,所以我們暫且停下對時(shí)延的討論?,F(xiàn)在,我們就要來看看“時(shí)間點(diǎn)”是怎么樣工作的。

4.3.3 時(shí)間點(diǎn)

時(shí)鐘的時(shí)間點(diǎn)可以用std::chrono::time_point<>的類型模板實(shí)例來表示,實(shí)例的第一個(gè)參數(shù)用來指定所要使用的時(shí)鐘,第二個(gè)函數(shù)參數(shù)用來表示時(shí)間的計(jì)量單位(特化的std::chrono::duration<>)。一個(gè)時(shí)間點(diǎn)的值就是時(shí)間的長度(在指定時(shí)間的倍數(shù)內(nèi)),例如,指定“unix時(shí)間戳”(epoch)為一個(gè)時(shí)間點(diǎn)。時(shí)間戳是時(shí)鐘的一個(gè)基本屬性,但是不可以直接查詢,或在C++標(biāo)準(zhǔn)中已經(jīng)指定。通常,unix時(shí)間戳表示1970年1月1日 00:00,即計(jì)算機(jī)啟動(dòng)應(yīng)用程序時(shí)。時(shí)鐘可能共享一個(gè)時(shí)間戳,或具有獨(dú)立的時(shí)間戳。當(dāng)兩個(gè)時(shí)鐘共享一個(gè)時(shí)間戳?xí)r,其中一個(gè)time_point類型可以與另一個(gè)時(shí)鐘類型中的time_point相關(guān)聯(lián)。這里,雖然你無法知道unix時(shí)間戳是什么,但是你可以通過對指定time_point類型使用time_since_epoch()來獲取時(shí)間戳。這個(gè)成員函數(shù)會(huì)返回一個(gè)時(shí)延值,這個(gè)時(shí)延值是指定時(shí)間點(diǎn)到時(shí)鐘的unix時(shí)間戳鎖用時(shí)。

例如,你可能指定了一個(gè)時(shí)間點(diǎn)std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>。這就與系統(tǒng)時(shí)鐘有關(guān),且實(shí)際中的一分鐘與系統(tǒng)時(shí)鐘精度應(yīng)該不相同(通常差幾秒)。

你可以通過std::chrono::time_point<>實(shí)例來加/減時(shí)延,來獲得一個(gè)新的時(shí)間點(diǎn),所以std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500)將得到500納秒后的時(shí)間。當(dāng)你知道一塊代碼的最大時(shí)延時(shí),這對于計(jì)算絕對時(shí)間的超時(shí)是一個(gè)好消息,當(dāng)?shù)却龝r(shí)間內(nèi),等待函數(shù)進(jìn)行多次調(diào)用;或,非等待函數(shù)且占用了等待函數(shù)時(shí)延中的時(shí)間。

你也可以減去一個(gè)時(shí)間點(diǎn)(二者需要共享同一個(gè)時(shí)鐘)。結(jié)果是兩個(gè)時(shí)間點(diǎn)的時(shí)間差。這對于代碼塊的計(jì)時(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í)例的時(shí)鐘參數(shù)可不僅是能夠指定unix時(shí)間戳的。當(dāng)你想一個(gè)等待函數(shù)(絕對時(shí)間超時(shí)的方式)傳遞時(shí)間點(diǎn)時(shí),時(shí)間點(diǎn)的時(shí)鐘參數(shù)就被用來測量時(shí)間。當(dāng)時(shí)鐘變更時(shí),會(huì)產(chǎn)生嚴(yán)重的后果,因?yàn)榈却壽E隨著時(shí)鐘的改變而改變,并且知道調(diào)用時(shí)鐘的now()成員函數(shù)時(shí),才能返回一個(gè)超過超時(shí)時(shí)間的值。當(dāng)時(shí)鐘向前調(diào)整,這就有可能減小等待時(shí)間的總長度(與穩(wěn)定時(shí)鐘的測量相比);當(dāng)時(shí)鐘向后調(diào)整,就有可能增加等待時(shí)間的總長度。

如你期望的那樣,后綴為_unitl的(等待函數(shù)的)變量會(huì)使用時(shí)間點(diǎn)。通常是使用某些時(shí)鐘的::now()(程序中一個(gè)固定的時(shí)間點(diǎn))作為偏移,雖然時(shí)間點(diǎn)與系統(tǒng)時(shí)鐘有關(guān),可以使用std::chrono::system_clock::to_time_point() 靜態(tài)成員函數(shù),在用戶可視時(shí)間點(diǎn)上進(jìn)行調(diào)度操作。例如,當(dāng)你有一個(gè)對多等待500毫秒的,且與條件變量相關(guān)的事件,你可以參考如下代碼:

清單4.11 等待一個(gè)條件變量——有超時(shí)功能

#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)你沒有什么事情可以等待時(shí),可在一定時(shí)限中等待條件變量。在這種方式中,循環(huán)的整體長度是有限的。如你在4.1.1節(jié)中所見,當(dāng)使用條件變量(且無事可待)時(shí),你就需要使用循環(huán),這是為了處理假喚醒。當(dāng)你在循環(huán)中使用wait_for()時(shí),你可能在等待了足夠長的時(shí)間后結(jié)束等待(在假喚醒之前),且下一次等待又開始了。這可能重復(fù)很多次,使得等待時(shí)間無邊無際。

到此,有關(guān)時(shí)間點(diǎn)超時(shí)的基本知識你已經(jīng)了解了?,F(xiàn)在,讓我們來了解一下如何在函數(shù)中使用超時(shí)。

4.3.4 具有超時(shí)功能的函數(shù)

使用超時(shí)的最簡單方式就是,對一個(gè)特定線程添加一個(gè)延遲處理;當(dāng)這個(gè)線程無所事事時(shí),就不會(huì)占用可供其他線程處理的時(shí)間。你在4.1節(jié)中看過一個(gè)例子,你循環(huán)檢查“done”標(biāo)志。兩個(gè)處理函數(shù)分別是std::this_thread::sleep_for()std::this_thread::sleep_until()。他們的工作就像一個(gè)簡單的鬧鐘:當(dāng)線程因?yàn)橹付〞r(shí)延而進(jìn)入睡眠時(shí),可使用sleep_for()喚醒;或因指定時(shí)間點(diǎn)睡眠的,可使用sleep_until喚醒。sleep_for()的使用如同在4.1節(jié)中的例子,有些事必須在指定時(shí)間范圍內(nèi)完成,所以耗時(shí)在這里就很重要。另一方面,sleep_until()允許在某個(gè)特定時(shí)間點(diǎn)將調(diào)度線程喚醒。這有可能在晚間備份,或在早上6:00打印工資條時(shí)使用,亦或掛起線程直到下一幀刷新時(shí)進(jìn)行視頻播放。

當(dāng)然,休眠只是超時(shí)處理的一種形式;你已經(jīng)看到了,超時(shí)可以配合條件變量和“期望”一起使用。超時(shí)甚至可以在嘗試獲取一個(gè)互斥鎖時(shí)(當(dāng)互斥量支持超時(shí)時(shí))使用。std::mutexstd::recursive_mutex都不支持超時(shí)鎖,但是std::timed_mutexstd::recursive_timed_mutex支持。這兩種類型也有try_lock_for()和try_lock_until()成員函數(shù),可以在一段時(shí)期內(nèi)嘗試,或在指定時(shí)間點(diǎn)前獲取互斥鎖。表4.1展示了C++標(biāo)準(zhǔn)庫中支持超時(shí)的函數(shù)。參數(shù)列表為“延時(shí)”(duration)必須是std::duration<>的實(shí)例,并且列出為時(shí)間點(diǎn)(time_point)必須是std::time_point<>的實(shí)例。

表4.1 可接受超時(shí)的函數(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)喚醒時(shí),返回謂詞的結(jié)果
wait_until(lock, duration, predicate)
std::timed_mutex 或 std::recursive_timed_mutex try_lock_for(duration) bool —— 獲取鎖時(shí)返回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)獲取鎖時(shí)返回true,否則返回false
try_lock_for(duration) bool —— 當(dāng)獲取鎖時(shí)返回true,否則返回false
try_lock_until(time_point)
std::future<ValueType>或std::shared_future<ValueType> wait_for(duration) 當(dāng)?shù)却瑫r(shí),返回std::future_status::timeout
wait_until(time_point) 當(dāng)“期望”準(zhǔn)備就緒時(shí),返回std::future_status::ready
當(dāng)“期望”持有一個(gè)為啟動(dòng)的延遲函數(shù),返回std::future_status::deferred

現(xiàn)在,我們討論的機(jī)制有:條件變量、“期望”、“承諾”還有打包的任務(wù)。是時(shí)候從更高的角度去看待這些機(jī)制,怎么樣使用這些機(jī)制,簡化線程的同步操作。