檢視原始碼 Erlang 中的時間與時間校正

擴展的時間功能

自 Erlang/OTP 18 (ERTS 7.0) 起,時間功能已擴展。這包括用於時間的新 API和當系統時間變更時會改變系統行為的時間扭曲模式

注意

自 Erlang/OTP 26 (ERTS 14.0) 起,多時間扭曲模式預設為啟用。這假設系統上執行的所有程式碼都是時間扭曲安全的。

如果系統中有舊的程式碼不是時間扭曲安全的,您現在需要明確地在無時間扭曲模式(或如果部分時間扭曲安全,則在單時間扭曲模式)中啟動系統,以避免問題。在無時間扭曲模式中啟動系統時,系統的行為會與 OTP 18 中引入擴展的時間功能之前相同。

如果您的程式碼不是時間扭曲安全的,強烈建議您變更此設定,以便您可以使用多時間扭曲模式。與無時間扭曲模式相比,多時間扭曲模式可以提高可擴展性和效能,以及時間測量的準確性和精確度。

術語

為了更容易理解本節,定義了一些術語。這是我們自己的術語(Erlang/OS 系統時間、Erlang/OS 單調時間、時間扭曲)和全球接受的術語的混合。

單調遞增

在單調遞增的值序列中,所有有前一個值的值都大於或等於其前一個值。

嚴格單調遞增

在嚴格單調遞增的值序列中,所有有前一個值的值都大於其前一個值。

UT1

世界時間。UT1 基於地球的自轉,在概念上是指經度 0° 的太陽時。

UTC

協調世界時。UTC 幾乎與 UT1 對齊。但是,UTC 使用秒的 SI 定義,其長度與 UT1 使用的秒長度並不完全相同。這表示 UTC 會慢慢偏離 UT1。為了使 UTC 與 UT1 保持相對同步,會插入,並且可能會刪除閏秒。也就是說,一個 UTC 日可以有 86400、86401 或 86399 秒長。

POSIX 時間

紀元以來的時間。紀元定義為 1970 年 1 月 1 日 00:00:00 UTCPOSIX 時間中的一天定義為正好 86400 秒長。奇怪的是,紀元被定義為 UTC 時間,而 UTC 對一天有多長有另一個定義。引用 Open Group 「因此,儘管 POSIX 時間看起來像 UTC,但它不一定是 UTC」。其效果是,當插入 UTC 閏秒時,POSIX 時間會停止一秒或重複最後一秒。如果刪除 UTC 閏秒(尚未發生過),POSIX 時間會向前跳一秒。

時間解析度

讀取時間值時可以區分的最小時間間隔。

時間精確度

讀取時間值時可以重複且可靠地區分的最小時間間隔。精確度受解析度限制,但解析度和精確度可能差異很大。

時間準確度

時間值的正確性。

時間扭曲

時間扭曲是時間向前或向後跳躍。也就是說,時間扭曲之前和之後取得的時間值之差不對應於實際經過的時間。

OS 系統時間

作業系統對POSIX 時間的檢視。若要擷取它,請呼叫 os:system_time()。這可能或可能不是 POSIX 時間的準確檢視。此時間通常可以在沒有限制的情況下向後和向前調整。也就是說,可能會觀察到時間扭曲

若要取得關於 Erlang 執行時間系統的 OS 系統時間來源的資訊,請呼叫 erlang:system_info(os_system_time_source)

OS 單調時間

OS 提供的單調遞增時間。此時間不會跳躍,並且具有相對穩定的頻率,雖然不是完全正確。但是,如果系統暫停,OS 單調時間停止並不罕見。此時間通常自時間中某些未指定點(與OS 系統時間無關)以來增加。並非所有 OS 都提供此類時間。

若要取得關於 Erlang 執行時間系統的 OS 單調時間來源的資訊,請呼叫 erlang:system_info(os_monotonic_time_source)

Erlang 系統時間

Erlang 執行時間系統對POSIX 時間的檢視。若要擷取它,請呼叫 erlang:system_time()

此時間可能或可能不是 POSIX 時間的準確檢視,並且可能或可能不與OS 系統時間對齊。執行時間系統會努力對齊兩個系統時間。根據使用的時間扭曲模式,這可以透過讓 Erlang 系統時間執行時間扭曲來達成。

Erlang 單調時間

Erlang 執行時間系統提供的單調遞增時間。Erlang 單調時間自時間中某些未指定點以來增加。若要擷取它,請呼叫 erlang:monotonic_time()

Erlang 單調時間的準確度精確度很大程度上取決於以下因素

在沒有 OS 單調時間的系統上,Erlang 單調時間保證單調性,但無法提供其他保證。對 Erlang 單調時間進行的頻率調整取決於使用的時間扭曲模式。

在執行時間系統的內部,Erlang 單調時間是「時間引擎」,用於或多或少所有與時間有關的事項。所有計時器,無論是 receive ... after 計時器、BIF 計時器還是timer 模組中的計時器,都是相對於 Erlang 單調時間觸發的。甚至Erlang 系統時間也是基於 Erlang 單調時間。將目前的 Erlang 單調時間與目前的時間偏移量相加,即可得到目前的 Erlang 系統時間。

若要擷取目前的時間偏移量,請呼叫 erlang:time_offset()

計時器

所有計時器都是相對於 Erlang 單調時間觸發的。目前所有計時器在 API 和執行時間系統內部都具有毫秒解析度。也就是說,解析度(以及精確度和準確度)不會高於毫秒。如果Erlang 單調時間的解析度低於毫秒,則計時器解析度也會低於毫秒。

計時器只能在自執行時間系統啟動以來的完整毫秒數觸發。不允許計時器在使用者給定的逾時時間之前觸發。也就是說,假設系統嚴重負載,當使用者給定逾時時間 T 時,計時器通常會在 [T, T+1) 毫秒範圍內觸發。如果系統嚴重負載,可能需要更長的時間才能觸發計時器。

簡介

時間對於 Erlang 程式至關重要,更重要的是,正確的時間對於 Erlang 程式至關重要。由於 Erlang 是一種具有軟即時特性的語言,而且我們可以在程式中表達時間,因此虛擬機器和語言必須小心考慮什麼是被認為是正確的時間,以及時間函數的行為方式。

在設計 Erlang 時,假設系統中的掛鐘時間顯示與時間定義完全相同的速度向前移動的單調時間。這或多或少意味著原子鐘(或更好的時間來源)預計會連接到您的硬體,然後預計硬體會永遠與任何人類操作隔絕。雖然這可能是一個引人入勝的想法,但事實並非如此。

「正常」的現代電腦無法自行計時,除非您有連接到它的晶片級原子鐘。您的電腦感知到的時間通常必須校正。因此,網路時間協定 (NTP) 協定與 ntpd 程序一起盡力使您的電腦時間與正確的時間同步。在 NTP 校正之間,通常會使用比原子鐘效力較低的時間保持器。

然而,NTP 並非萬無一失。NTP 伺服器可能無法使用、ntp.conf 可能配置錯誤,或者您的電腦有時可能會斷開與網際網路的連線。此外,您可能會有使用者(甚至系統管理員)認為處理日光節約時間的正確方法是一年兩次調整時鐘一小時(這是不正確的做法)。更複雜的是,這位使用者從網際網路取得了您的軟體,並且沒有考慮到電腦所認知的正確時間。使用者不在乎讓牆上時鐘與正確時間同步。使用者期望您的程式擁有關於時間的無限知識。

大多數程式設計師也期望時間是可靠的,至少在他們意識到他們工作站上的牆上時鐘時間慢了一分鐘之前。然後他們會將其設定為正確的時間,但很可能不是以平滑的方式進行。

當您總是期望系統上的牆上時鐘時間正確時,可能會產生大量的問題。因此,Erlang 多年前引入了「時間的修正估計值」,或稱為「時間校正」。時間校正是基於大多數作業系統都有某種單調時脈的事實,無論是即時擴充功能還是獨立於牆上時鐘設定的內建「滴答計數器」。此計數器可能具有微秒解析度或更低的解析度,但它具有不可忽略的漂移。

時間校正

如果啟用時間校正,Erlang 執行時系統會同時使用 作業系統系統時間作業系統單調時間,來調整 Erlang 單調時脈的頻率。時間校正確保 Erlang 單調時間 不會扭曲,並且頻率相對準確。頻率調整的類型取決於所使用的時間扭曲模式。章節 時間扭曲模式 提供了更多詳細資訊。

預設情況下,如果特定平台上存在支援,則會啟用時間校正。其支援包括作業系統提供的作業系統單調時間,以及 Erlang 執行時系統中使用作業系統單調時間的實作。要檢查您的系統是否支援作業系統單調時間,請呼叫 erlang:system_info(os_monotonic_time_source)。要檢查您的系統是否啟用了時間校正,請呼叫 erlang:system_info(time_correction)

要啟用或停用時間校正,請將命令列引數 +c [true|false] 傳遞給 erl(1)

如果停用時間校正,Erlang 單調時間可能會向前扭曲、停止,甚至凍結很長一段時間。那麼就無法保證 Erlang 單調時脈的頻率是準確的或穩定的。

您通常永遠不會想要停用時間校正。以前時間校正與效能損失有關,但現在通常情況正好相反。如果停用時間校正,您可能會遇到不良的可擴展性、不良的效能以及不良的時間測量。

時間扭曲安全程式碼

時間扭曲安全程式碼可以處理 Erlang 系統時間時間扭曲

當 Erlang 系統時間扭曲時,erlang:now/0 的行為會很糟糕。當 Erlang 系統時間向後時間扭曲時,從 erlang:now/0 傳回的值會凍結(如果忽略由於實際呼叫而產生的微秒增量),直到作業系統系統時間到達 erlang:now/0 傳回的最後一個值的位置。這種凍結可能會持續很長時間。它可能會持續數年、數十年,甚至更長時間,直到凍結停止。

並非所有使用 erlang:now/0 的情況都是時間扭曲不安全的。如果您不使用它來取得時間,那麼它是時間扭曲安全的。但是,從效能和可擴展性的角度來看,所有使用 erlang:now/0 的情況都是次佳的。因此,您真的希望將其使用替換為其他功能。有關如何替換 erlang:now/0 的使用的範例,請參閱章節 如何使用新的 API

時間扭曲模式

目前的 Erlang 系統時間 是透過將目前的 Erlang 單調時間 與目前的 時間偏移 相加來確定的。時間偏移的處理方式取決於您使用的時間扭曲模式。

要設定時間扭曲模式,請將命令列引數 +C [no_time_warp|single_time_warp|multi_time_warp] 傳遞給 erl(1)

無時間扭曲模式

時間偏移在執行時系統啟動時確定,之後不會變更。這與 OTP 26 (ERTS 14.0) 之前的預設行為相同,也是 OTP 18 (ERTS 7.0) 之前的唯一行為。

由於不允許變更時間偏移,時間校正必須調整 Erlang 單調時脈的頻率,以使 Erlang 系統時間與作業系統系統時間平穩對齊。這種方法的一個重大缺點是,如果需要調整,我們會有意在 Erlang 單調時脈上使用錯誤的頻率。此錯誤可能高達 1%。此錯誤將顯示在執行時系統中的所有時間測量中。

如果未啟用時間校正,當作業系統系統時間向後跳躍時,Erlang 單調時間會凍結。單調時間的凍結會持續到作業系統系統時間趕上為止。凍結可能會持續很長時間。當作業系統系統時間向前跳躍時,Erlang 單調時間也會向前跳躍。

單次時間扭曲模式

從其引入以來,此模式或多或少是一種向後相容模式。

在嵌入式系統中,系統關閉時沒有電源供應(甚至沒有電池)是很常見的。此類系統上的系統時鐘在系統開機時通常會嚴重偏差。如果使用 無時間扭曲模式,並且在作業系統系統時間被校正之前啟動了 Erlang 執行時系統,Erlang 系統時間可能會錯誤很長時間,數個世紀甚至更長。

如果您需要使用不是 時間扭曲安全 的 Erlang 程式碼,並且需要在作業系統系統時間被校正之前啟動 Erlang 執行時系統,您可能需要使用單次時間扭曲模式。

注意

使用此模式執行時間扭曲不安全程式碼的時間有限制。如果有可能只使用時間扭曲安全程式碼,那麼使用 多次時間扭曲模式好得多

使用單次時間扭曲模式時,時間偏移的處理分為兩個階段

  • 初步階段 - 此階段在執行時系統啟動時開始。會根據目前的作業系統系統時間確定初步時間偏移。此偏移從現在開始在整個初步階段固定不變。

    如果啟用時間校正,則會對 Erlang 單調時脈進行調整,以使其頻率盡可能正確。但是,不會進行任何調整以嘗試對齊 Erlang 系統時間和作業系統系統時間。也就是說,在初步階段,Erlang 系統時間和作業系統系統時間可能會彼此發散,並且不會嘗試阻止這種情況。

    如果停用時間校正,作業系統系統時間的變更會以與使用 無時間扭曲模式 時相同的方式影響單調時脈。

  • 最終階段 - 當使用者透過呼叫 erlang:system_flag(time_offset, finalize) 完成時間偏移時,此階段開始。完成只能執行一次。

    在完成期間,會調整並固定時間偏移,以使目前的 Erlang 系統時間與目前的作業系統系統時間對齊。由於時間偏移可能會在完成期間變更,Erlang 系統時間可能會在此時進行時間扭曲。從現在開始,時間偏移會被固定,直到執行時系統終止。如果已啟用時間校正,則從現在開始,時間校正也會進行調整,以使 Erlang 系統時間與作業系統系統時間對齊。當系統處於最終階段時,其行為與 無時間扭曲模式 中的行為完全相同。

為了使此功能正常運作,使用者必須確保滿足以下兩個要求

  • 向前時間扭曲 - 完成時間偏移時進行的時間扭曲只能向前進行,而不會遇到問題。這表示使用者必須確保在啟動 Erlang 執行時系統之前,作業系統系統時間設定為早於或等於實際 POSIX 時間的時間。

    如果您不確定作業系統系統時間是否正確,請在啟動 Erlang 執行時系統之前,為了安全起見,將其設定為保證早於實際 POSIX 時間的時間。

  • 完成正確的作業系統系統時間 - 當使用者完成時間偏移時,作業系統系統時間必須是正確的。

如果未滿足這些要求,系統的行為可能會非常糟糕。

假設滿足這些要求,並且啟用時間校正,並且使用諸如 NTP 之類的時間調整協定來調整作業系統系統時間,則只需要對 Erlang 單調時間進行小幅調整,即可在完成後使系統時間保持對齊。只要系統未暫停,所需的最大調整是針對插入(或刪除)的閏秒。

警告

要使用此模式,請確保在兩個階段中執行的所有 Erlang 程式碼都是 時間扭曲安全 的。

僅在最終階段執行的程式碼不必能夠應對時間扭曲。

多次時間扭曲模式

多次時間扭曲模式與時間校正結合是首選的組態。這是因為 Erlang 執行時系統具有更好的效能、更好的擴展性,並且在幾乎所有平台上都有更好的行為。此外,時間測量的準確性和精確度也更好。只有在舊平台上執行的 Erlang 執行時系統才能從其他組態中受益。截至 OTP 26 (ERTS 14.0),這也是預設值。

時間偏移量可以隨時更改,沒有任何限制。也就是說,Erlang 系統時間可以隨時向前或向後進行時間跳躍。當我們通過更改時間偏移量來使 Erlang 系統時間與作業系統時間對齊時,我們可以啟用時間校正,盡可能地調整 Erlang 單調時鐘的頻率,使其盡可能準確。這使得使用 Erlang 單調時間進行的時間測量更加準確和精確。

如果停用時間校正,當作業系統時間向前跳躍時,Erlang 單調時間會向前跳躍。如果作業系統時間向後跳躍,Erlang 單調時間會短暫停止,但不會長時間凍結。這是因為時間偏移量已更改為使 Erlang 系統時間與作業系統時間對齊。

警告

要使用此模式,請確保所有將在執行階段系統上執行的 Erlang 程式碼都是時間跳躍安全的。

新的時間 API

舊的時間 API 基於 erlang:now/0erlang:now/0 的用途是處理許多不相關的事情。這將這些不相關的操作綁定在一起,並導致效能、可擴展性、準確性和精確性方面的問題,而這些操作原本不需要有這些問題。為了改善這一點,新的 API 將不同的功能分散到多個函式中。

為了向後相容,erlang:now/0 保持「原樣」,但強烈建議不要使用它erlang:now/0 的許多使用案例會妨礙您使用新的多時間跳躍模式,這是此新的時間功能改進的重要組成部分。

在某些系統上,一些新的 BIF(內建函式)在剛啟動的執行階段系統上可能會傳回負整數值,這可能令人驚訝。這不是錯誤,而是一種記憶體使用優化。

新的 API 包含以下新的 BIF:

新的 API 也包含以下現有 BIF 的擴展:

新的 Erlang 單調時間

Erlang 單調時間是從 ERTS 7.0 開始新增的。引入它是為了將時間測量(例如經過的時間)與日曆時間分離。在許多使用案例中,需要測量經過的時間或指定相對於另一個時間點的時間,而無需知道 UTC 或任何其他全局定義的時間尺度中的相關時間。通過引入一個時間尺度,其中有關於它開始位置的本地定義,則可以在該時間尺度上管理與日曆時間無關的時間。Erlang 單調時間使用這樣的時間尺度,並具有本地定義的開始時間。

引入 Erlang 單調時間允許我們單獨調整兩個 Erlang 時間(Erlang 單調時間和 Erlang 系統時間)。通過這樣做,經過時間的準確性不必僅僅因為系統時間在某個時間點發生錯誤而受到影響。只有在時間跳躍模式下才會執行對兩個時間的單獨調整,並且只有在多時間跳躍模式下才能完全分離。除了多時間跳躍模式之外的所有其他模式都是為了向後相容的原因。當使用這些模式時,Erlang 單調時間的準確性會受到影響,因為在這些模式下對 Erlang 單調時間的調整或多或少與 Erlang 系統時間綁定。

系統時間的調整本可以比使用時間跳躍方法更平滑,但我們認為這將是一個糟糕的選擇。由於我們可以使用 Erlang 單調時間來表達和測量與日曆時間無關的時間,因此最好立即顯示 Erlang 系統時間的變化。這是因為在系統上執行的 Erlang 應用程式可以盡快對系統時間的變化做出反應。這也或多或少與大多數作業系統處理此問題的方式相同(作業系統單調時間和作業系統系統時間)。通過平穩地調整系統時間,我們只是隱藏了系統時間已變更的事實,並使 Erlang 應用程式更難以合理的方式對變更做出反應。

為了能夠對 Erlang 系統時間的變更做出反應,您必須能夠檢測到它發生了。當目前時間偏移量變更時,Erlang 系統時間會發生變更。因此,我們引入了使用 erlang:monitor(time_offset, clock_service) 監視時間偏移量的可能性。當時間偏移量變更時,監視時間偏移量的處理序會收到以下格式的訊息

{'CHANGE', MonitorReference, time_offset, clock_service, NewTimeOffset}

唯一值

除了報告時間之外,erlang:now/0 還會產生唯一且嚴格單調遞增的值。為了將此功能與時間測量分離,我們引入了 erlang:unique_integer()

如何使用新的 API

以前,erlang:now/0 是執行許多操作的唯一選項。本節介紹 erlang:now/0 可以用於某些操作的方式,以及如何使用新的 API。

擷取 Erlang 系統時間

不要

使用 erlang:now/0 擷取目前的 Erlang 系統時間。

應該

使用 erlang:system_time/1 在您選擇的 時間單位上擷取目前的 Erlang 系統時間。

如果您想要與 erlang:now/0 傳回的格式相同的格式,請使用 erlang:timestamp/0

測量經過的時間

不要

使用 erlang:now/0 擷取時間戳記,並使用 timer:now_diff/2 計算時間差。

應該

使用 erlang:monotonic_time/0 擷取時間戳記,並使用普通的減法運算計算時間差。結果以 native 時間單位表示。如果要將結果轉換為另一個時間單位,可以使用 erlang:convert_time_unit/3

更簡單的方法是使用具有所需時間單位的 erlang:monotonic_time/1。但是,您可能會損失準確性和精確度。

確定事件的順序

不要

通過在事件發生時使用 erlang:now/0 儲存時間戳記來確定事件的順序。

應該

通過儲存事件發生時 erlang:unique_integer([monotonic]) 傳回的整數來確定事件的順序。這些整數在目前的執行階段系統執行個體上會根據建立時間嚴格單調排序。

確定具有事件時間的事件順序

不要

通過在事件發生時使用 erlang:now/0 儲存時間戳記來確定事件的順序。

應該

通過儲存一個包含 單調時間嚴格單調遞增的整數的元組來確定事件的順序,如下所示

Time = erlang:monotonic_time(),
UMI = erlang:unique_integer([monotonic]),
EventTag = {Time, UMI}

這些元組在目前的執行階段系統執行個體上會根據建立時間嚴格單調排序。重要的是,單調時間位於第一個元素中(比較雙元組時最重要的元素)。使用元組中的單調時間,您可以計算事件之間的時間。

如果您對事件發生時的 Erlang 系統時間感興趣,您還可以使用 erlang:time_offset/0 在儲存事件之前或之後儲存時間偏移量。Erlang 單調時間加上時間偏移量對應於 Erlang 系統時間。

如果您在時間偏移量可能變更的模式下執行,並且想要取得事件發生時的實際 Erlang 系統時間,則可以在元組中將時間偏移量儲存為第三個元素(比較三元組時最不重要的元素)。

建立唯一名稱

不要

使用 erlang:now/0 傳回的值在目前的執行階段系統執行個體上建立唯一名稱。

應該

使用 erlang:unique_integer/0 傳回的值在目前的執行階段系統執行個體上建立唯一名稱。如果您只想要正整數,可以使用 erlang:unique_integer([positive])

使用唯一值為隨機數產生器設定種子

不要

使用 erlang:now/0 為隨機數產生器設定種子。

應該

使用 erlang:monotonic_time/0erlang:time_offset/0erlang:unique_integer/0 和其他功能的組合來為隨機數產生器設定種子。

總結本節:不要使用 erlang:now/0

支援新舊 OTP 版本

可能需要您的程式碼在不同 OTP 版本的各種 OTP 安裝上執行。如果是這樣,您無法立即使用新的 API,因為它在 OTP 18 之前的版本上不可用。解決方案不是避免使用新的 API,因為這樣您的程式碼將無法從所做的可擴展性和準確性改進中受益。相反,請在可用時使用新的 API,並且在新的 API 不可用時回退到 erlang:now/0

幸運的是,除了以下內容之外,大多數新的 API 都可以使用現有的基本函數輕鬆實作:

通過使用在新的 API 不可用時回退到 erlang:now/0 的函式包裝 API,並使用這些包裝器而不是直接使用 API,就可以解決這個問題。這些包裝器例如可以在 $ERL_TOP/erts/example/time_compat.erl 中實作。