1. 程式人生 > 程式設計 >C++11中的時間庫std::chrono(引發關於時間的思考)

C++11中的時間庫std::chrono(引發關於時間的思考)

前言

時間是寶貴的,我們無時無刻不在和時間打交道,這個任務明天下班前截止,你點的外賣還有5分鐘才能送到,那個程式已經運行了整整48個小時,既然時間和我們聯絡這麼緊密,我們總要定義一些術語來描述它,像前面說到的明天下班前、5分鐘、48個小時都是對時間的描述,程式程式碼構建的程式世界也需要定義一些術語來描述時間。

今天要總結學習的是 std::chrono 庫,它是 C++11 標準時從 boost 庫中引入的,其實在 C++ 中還有一種 C 語言風格的時間管理體系,像我們常見的函式 time()clock()localtime()mktime() 和常見的型別 tmtime_tclock_t

都是 C 語言風格的時間管理體系。

std::chrono 這個庫之前接觸的不多,C++20 標準都出了,C++11 引入的這個庫還沒怎麼用過,整天和 time()localtime()tm 打交道,最近工作中換了專案,程式碼中出現了 std::chrono 的使用,是時候好好學習總結一下了。

chrono 的概況頭

檔案#include <chrono>

名稱空間 std::chrono

這個庫從 C++11 引入標準之後,每個版本都有所修改,不過核心內容變化不是太大,他定義了三種主要型別,分別是 durationsclockstime points,以及圍繞這些型別的一些工具函式和衍生的定義。

chrono 的核心內容duration

這個模板類用來表示時間間隔,我們知道時間的基本單位是秒,這個類的物件所表示的時間間隔也是以秒為單位的,它的定義如下:

template<class Rep,class Period = std::ratio<1>>
class duration;

Rep 表示一種數值型別,用來描述週期 Period 的數值型別,比如可以是 intfloat 等,而 Period 的型別是 std::ratio,同樣是一個模板類,實際表示的是一個有理數,像100、0、1/1000(千分之一)等等。

std 這個名稱空間下有很多已經定義好的有理數,可以舉幾個常見的標頭檔案 <ratio>

中的例子:

nano std::ratio<1,1000000000> // 十億分之一
micro std::ratio<1,1000000> // 百萬分之一
milli std::ratio<1,1000>  // 千分之一
centi std::ratio<1,100>  // 百分之一
deci std::ratio<1,10>  // 十分之一
deca std::ratio<10,1>  // 十
hecto std::ratio<100,1>  // 百
kilo std::ratio<1000,1>  // 千

比如我們想定義一個整數型別的100秒的時間間隔型別可以使用:

typedef std::chrono::duration<int,std::ratio<100,1>> my_duration_type;

當然也可以簡寫成:

typedef std::chrono::duration<int,std::hecto> my_duration_type;

如果我們想定義一個整數型別1分鐘的時間間隔型別可以寫成:

typedef std::chrono::duration<int,std::ratio<60,1>> my_minute_type;

因為這種時、分、秒的時間表示在程式碼邏輯中很常用,所有在 std::chrono 名稱空間下已經定義好了一些時間間隔型別:

std::chrono::nanoseconds duration</*signed integer type of at least 64 bits*/,std::nano>
std::chrono::microseconds duration</*signed integer type of at least 55 bits*/,std::micro>
std::chrono::milliseconds duration</*signed integer type of at least 45 bits*/,std::milli>
std::chrono::seconds duration</*signed integer type of at least 35 bits*/>
std::chrono::minutes duration</*signed integer type of at least 29 bits*/,std::ratio<60>>
std::chrono::hours  duration</*signed integer type of at least 23 bits*/,std::ratio<3600>>

另外還有一個很重要的成員函式 count(),用來獲得指定的時間間隔物件中包含多少個時間週期,接下來可以寫個例子理解一下,我們用 duration 這個模板類來表示一下5分鐘和12小時,看看他應該怎麼使用,對於5分鐘你可以看成是 5 個 1 分鐘或者 1 個 5 分鐘,或者更變態你可以看成 2.5 個 2 分鐘,而 12 小時一般會看成是 12個 1 小時,你當成 0.5 個 1 天也是可以的:

#include <chrono>
#include <iostream>
int main()
{
 // 以下為5分鐘表達
 std::chrono::minutes minute1{5}; // 5個1分鐘
 std::chrono::duration<int,std::ratio<5*60,1>> minute2{1}; // 1個5分鐘
 std::chrono::duration<double,std::ratio<2*60,1>> minute3{2.5}; // 2.5個2分鐘

 std::cout << "minutes1 duration has " << minute1.count() << " ticks\n"
  << "minutes2 duration has " << minute2.count() << " ticks\n"
  << "minutes3 duration has " << minute3.count() << " ticks\n";

 // 一下為12小時表達
 std::chrono::hours hours1{12}; // 12個1小時
 std::chrono::duration<double,std::ratio<60*60*24,1>> hours2{0.5}; // 0.5個1天

 std::cout << "hours1 duration has " << hours1.count() << " ticks\n"
  << "hours2 duration has " << hours2.count() << " ticks\n";

 // 使用 std::chrono::duration_cast<T> 將分鐘間隔轉化成標準秒間隔
 std::cout << "minutes1 duration has " <<
 std::chrono::duration_cast<std::chrono::seconds>(minute1).count() << " seconds\n";
}

上述程式碼中還使用了 std::chrono::duration_cast<T>() 函式,用於各種時間間隔的換算,執行結果如下:

minutes1 duration has 5 ticks
minutes2 duration has 1 ticks
minutes3 duration has 2.5 ticks
hours1 duration has 12 ticks
hours2 duration has 0.5 ticks
minutes1 duration has 300 seconds

clock

從名字可以看出這個類叫做時鐘,時鐘是用來看時間和計時的,常用的兩個類是 system_clocksteady_clock,在 C++20 標準中又加入了多種內容,現在我們先來看看這兩個常用類。

從這一部分開始類的定義讓人有些迷糊,其實 clock 引用了 std::chrono::duration 和後面要說的 std::chrono::time_point, 而 std::chrono::time_point 又引用了 std::chrono::duration 和現在要講的 std::chrono::system_clockstd::chrono::steady_clock,如果只看定義很容易被繞暈,所以還是先做個練習實驗一下。

system_clock

這個類被稱為系統內時鐘,當修改系統時鐘時可能會改變其單調遞增的性質,靜態成員函式有 now()to_time_t()from_time_t() 三個,關於它的單調性被修改舉個例子,一般認為時間一直是遞增的,但是當你現在呼叫一次函式 now(),然後把時間往過去調1天,然後再呼叫 now() 函式,就會發現新得到的時間“變小”了。

也因為這樣它會受到 NTP(Network Time Protocol,網路時間協議)的影響,但是不會受時區和夏令時的影響(其實很多國家早就廢除夏令時了)。

下面寫個例子練習一下,例子中使用了 now()to_time_t()from_time_t() 三個函式,不清楚的時候可以對照一下:

#include <chrono>
#include <iostream>
int main()
{
 std::chrono::duration<int,std::ratio<60*60*24> > one_day(1);

 // 根據時鐘得到現在時間
 std::chrono::system_clock::time_point today = std::chrono::system_clock::now();
 std::time_t time_t_today = std::chrono::system_clock::to_time_t(today);
 std::cout << "now time stamp is " << time_t_today << std::endl;
 std::cout << "now time is " << ctime(&time_t_today) << std::endl;


 // 看看明天的時間
 std::chrono::system_clock::time_point tomorrow = today + one_day;
 std::time_t time_t_tomorrow = std::chrono::system_clock::to_time_t(tomorrow);
 std::cout << "tomorrow time stamp is " << time_t_tomorrow << std::endl;
 std::cout << "tomorrow time is " << ctime(&time_t_tomorrow) << std::endl;


 // 計算下個小時時間
 std::chrono::system_clock::time_point next_hour = today + std::chrono::hours(1);
 std::time_t time_t_next_hour = std::chrono::system_clock::to_time_t(next_hour);
 std::chrono::system_clock::time_point next_hour2 = std::chrono::system_clock::from_time_t(time_t_next_hour);

 std::time_t time_t_next_hour2 = std::chrono::system_clock::to_time_t(next_hour2);
 std::cout << "tomorrow time stamp is " << time_t_next_hour2 << std::endl;
 std::cout << "tomorrow time is " << ctime(&time_t_next_hour2) << std::endl;

 return 0;
}

執行結果如下:

now time stamp is 1586662332
now time is Sun Apr 12 11:32:12 2020

tomorrow time stamp is 1586748732
tomorrow time is Mon Apr 13 11:32:12 2020

tomorrow time stamp is 1586665932
tomorrow time is Sun Apr 12 12:32:12 2020

steady_clock

這是一個單調時鐘,一旦啟動之後就與系統時間沒有關係了,完全根據物理是時間向前移動,成員函式只有一個 now(),通常可以用來計時,使用方法與 system_clock 相比簡單許多,下面寫個小例子。

#include <chrono>
#include <iostream>
int main()
{
 // 先記錄程式執行時間
 std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();

 volatile int nDstVal,nSrcVal;
 for (int i = 0; i < 1000000000; ++i)
 nDstVal = nSrcVal;

 // 做差值計算耗時
 std::chrono::duration<double> duration_cost = std::chrono::duration_cast<
 std::chrono::duration<double> >(std::chrono::steady_clock::now() - start);
 std::cout << "total cost " << duration_cost.count() << " seconds." << std::endl;

 return 0;
}

執行結果如下:

total cost 1.9424 seconds.

time point

這個類與 duration 類似,同樣是模板類,表示具體的時間點,比如今天 18:00 開飯,明天上午 10:00 發版本,今年 5 月 1 日可能因為疫情不讓出去玩了,像這些具體的時間點可以使用 std::chrono::time_point 來表達,它的定義如下:

template<class Clock,class Duration = typename Clock::duration>
class time_point;

首先這個類是在 std::chrono 這個名稱空間下,但是你會經常看到以下這種寫法:

std::chrono::system_clock::time_point today = std::chrono::system_clock::now();
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();

好像 time_point 又在 std::chrono::system_clockstd::chrono::steady_clock 範圍內,實際上這兩個範圍內的 time_point 引用的是 std::chrono::time point,看看 std::chrono::system_clock 的定義能明白一些。

class system_clock {
public:
 using rep = /*see description*/ ;
 using period = ratio</*unspecified*/,/*unspecified*/ >;
 using duration = chrono::duration<rep,period>;
 using time_point = chrono::time_point<system_clock>;
 static constexpr bool is_steady = /*unspecified*/ ;
 static time_point now() noexcept;
 // Map to C API
 static time_t to_time_t (const time_point& t) noexcept;
 static time_point from_time_t(time_t t) noexcept;
};

對照上面的定義可以知道,std::chrono::system_clock::time_point 實際上 std::chrono::time_point<system_clock>,這幾個時間類的定義相互引用,看到這一部分的時候一定不要煩躁,一步步推導分析其中的關係。

time_point 這個類有一個成員函式 time_since_epoch() 用來獲得 1970-01-01 00:00:00time_point 時間經過的 duration,返回的 duration 的單位取決於 timepoint 定義時的 duraion 的單位,不過你也可以得到 duration 之後使用 std::chrono::duration_cast<T>() 函式來轉化。

#include <chrono>
#include <iostream>
int main()
{
 // 獲得epoch 和 now 的時間點
 std::chrono::time_point<std::chrono::system_clock> epoch =
 std::chrono::time_point<std::chrono::system_clock>{};
 std::chrono::time_point<std::chrono::system_clock> now =
 std::chrono::system_clock::now();

 // 顯示時間點對應的日期和時間
 time_t epoch_time = std::chrono::system_clock::to_time_t(epoch);
 std::cout << "epoch: " << std::ctime(&epoch_time);
 time_t today_time = std::chrono::system_clock::to_time_t(now);
 std::cout << "today: " << std::ctime(&today_time);

 // 顯示duration的值
 std::cout << "seconds since epoch: "
 << std::chrono::duration_cast<std::chrono::seconds>(epoch.time_since_epoch()).count()
 << std::endl;

 std::cout << "today,ticks since epoch: "
 << now.time_since_epoch().count()
 << std::endl;

 std::cout << "today,hours since epoch: "
 << std::chrono::duration_cast<std::chrono::hours>(now.time_since_epoch()).count()
 << std::endl;

 return 0;
}

執行結果如下:

epoch: Thu Jan 1 08:00:00 1970
today: Sun Apr 12 12:30:04 2020
seconds since epoch: 0
today,ticks since epoch: 1586665804624992500
today,hours since epoch: 440740

從執行結果來看,epoch 的時間點是 Thu Jan 1 08:00:00 1970,為什麼不是 1970-01-01 00:00:00 呢?那是因為我們在東8區,格林威治時間為
1970-01-01 00:00:00 的時候,我們的時間就是 Thu Jan 1 08:00:00 1970,這樣看來 std::ctime() 這個函式考慮了時區的影響,相同的程式碼如果在韓國同時執行得到的可能就是 epoch: Thu Jan 1 09:00:00 1970

關於時間的思考

思考一個問題,時間是不是一種不變的量,或者換一種說法,它是不是一種均勻的量。如果瞭解過《三體》中的部分章節,你就會發現時間總在被任意改變著。但是在現實生活中好像時間就是一個標準,我們認為它是一成不變的,總是感覺今天的1天和昨天的24小時在時間上是等同的,今年的這一年和去年的365天是等同的,但其實你瞭解一下閏年、閏秒、夏令時就會發現,前面提到的這些未必等同。

日常生活中對時間的描述只是為了理解和闡明一些事物,我們把太陽升到頭頂叫做中午,把地球自轉一圈叫做一天24小時,把地球圍繞太陽公轉一圈叫做1年365天,但是地球自轉不是那麼均勻的,也就是說每轉一圈佔用的絕對時間是不一樣的,我們現在使用的時鐘通常是滴答滴答一秒秒的走著,如果地球自轉一圈的時間不是完全相同的,那麼建立在這個滴答上的一切時間都是不準確的。

什麼是建立在滴答滴答上的時間,我們以滴答一次作為1秒來計算,那麼1分鐘是60秒,也就是滴答60次,1小時是60分鐘,滴答3600次,一天是24小時,滴答86400次,滴答的次數是均勻的,但是自轉和公轉是不均勻的,那麼兩個時間就對不上了,所以出現了閏秒、閏年等方法來調整時間,使得我們用來描述生活的時間和周圍的環境現象可以一致,不然大約幾千年以後就會出現中午12點天上出現月亮的奇觀,那時的人們在史書中會發現我們這個時代中午12點掛在天上的是太陽,簡直太玄幻。

有沒有一種計時可以描述這種不均勻的自轉呢?其實我們偉大的古人早已經發明出來了,你一定聽說過日晷這種計時工具,它是觀測日影記時的儀器,主要是根據日影的在日晷面上的位置,以指定當時的時辰或刻數,是我國古代較為普遍使用的計時儀器。為什麼它沒有時間不一致的問題?因為它本身就是不均勻的,它是根據自然現象來規定生活中每天的時間的,其實對照現在來說就是每個時辰的滴答數實際上是不一樣的。

日晷這種不均勻的計時其實是為了適應天文現象,方便人們的生產生活,所以說現在地球自轉一圈是一天,但不一定是86400秒,地球公轉一圈是一年,但不一定是365天,後來人們使用電子裝置計時,按道理來說應該非常準確,但是因為地球自轉、公轉的速率都不穩定,這種差距漸漸地會給生活帶來困擾,於是又發明了一個折中的協調世界時,會在適當的時候閏秒、閏天,以彌補這種差距。假如你買了一個絕對精準的不聯網的電子計時器,但是幾年之後你就會發現你的計時器肯定和大家使用的標準時間不一致了。

其實還有一種基於特定銫原子的振盪週期來確定的國際原子時,主要是在時間精度要求較高的航天、通訊、電子等領域,為了保持系統的連續性而使用的,在日常生活中基本不會使用,但是這個時間是相對恆定的,不會去計較天文現象,每一秒都“準確”的流逝著。

時間函式思考

現在回過頭來再來看這些時間函式,是不是感覺有點不一樣了,比如 time(NULL) 這個函式,它返回的是從 1970-01-01 00:00:00 到現在時間的秒數,回憶一下上面關於時間的思考,這個秒數真的是準確的嗎?其實你如果理解了上面的內容就能得出結論,它肯定和國際原子時是有出入的。

再考慮下閏秒的影響,假如你實現了一個函式,第一次執行是在0點執行,執行之後你設定了一個86400秒的倒計時,也就是1天的時間,到第二天0點的時候正好又執行,你又設定了一個86400秒的倒計時,但今天正好是閏秒的日子,也就是今天會比昨天多1秒,那麼今天的時間到23:59:59的時候就經過了86400秒,也就是說在23:59:59的時候就會執行你寫的函式,如果碰到秒殺就尷尬了…

一般的程式開發不用太考慮閏秒的影響,但是如果這一秒的誤差出現的宇宙飛船的飛行中,可能會導致幾十公里的誤差,所以程式設計師們一定要理解閏秒的可能帶來的問題,評估自己所寫的程式碼需不需要處理這種情況。曾經的一次閏秒直接導致了芬蘭航空系統的癱瘓,所以一些大型專案還是會提前很長時間就把即將到來的閏秒處理寫入到自己的系統中,以應對它帶來的危險。

當你認為時間不會倒流的時候,它確實就發生了。我們一般假定時間不會倒流,但是如果你過分依賴這個特性,可能就會導致一些問題,這種情況常常出現設定了自動校準時間的電腦上,電腦的時間走快了,然後到達一定的差距後會觸發校準程式,這時就會出現“時間倒流”的現象,比如 time(NULL) 這種依賴於電腦時間的函式,在這種情況下函式返回值就會變小,出現不單調性。

總結關於時間的操作真的太多了,我居然發現一種名為 operator""h 的操作符,與數字連用表示小時,有興趣的話可以自己擴充套件學習一下。durationsclockstime points 三種有關時間操作的定義相互之間是有引用的,需要理清其中的關係。需要了解閏秒、閏年、天文時、原子時、協調時產生的原因,這樣就可以做到熟悉原理,心裡不慌。在測試的例子中出現了時區的概念,其實是人們為了生產生活主動創造出來以適應自然現象的。這裡丟擲一個疑問,我之前剛接觸時暈乎了很久,後來漸漸才明白,有些時間函式的說明中會提到與時區無關,比如 time(NULL)、還有今天學習的 system_clock,但是當我修改電腦時區的時候會發現,這些函式的返回值會發生突變,大家有探究過其中的原因嗎?

我們都是追逐時間奔跑的螻蟻,改變世界的同時也被時間改變著。

總結

到此這篇關於C++11中的時間庫std::chrono(引發關於時間的思考)的文章就介紹到這了,更多相關C++11 時間庫 std::chrono 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!