查看源代码 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-01-01 00:00:00 UTCPOSIX 时间中的一天被定义为正好 86400 秒长。奇怪的是,纪元被定义为 UTC 时间,而 UTC 对一天的长度有另一种定义。引用 Open Group 的话“因此,POSIX 时间不一定就是 UTC,尽管它看起来像”。这样做的效果是,当插入 UTC 闰秒时,POSIX 时间会停止一秒钟,或者重复最后一秒。如果删除 UTC 闰秒(尚未发生过),POSIX 时间将向前跳跃一秒。

时间分辨率

读取时间值时可以区分的最短时间间隔。

时间精度

读取时间值时可以重复且可靠地区分的最短时间间隔。精度受分辨率的限制,但分辨率和精度可能存在显着差异。

时间准确度

时间值的正确性。

时间跳变

时间跳变是指时间向前或向后跳跃。也就是说,时间跳变前后获取的时间值之差与实际经过的时间不对应。

OS 系统时间

操作系统对 POSIX 时间的看法。要检索它,请调用 os:system_time()。这可能(也可能不)是 POSIX 时间的准确视图。此时间通常可以不受限制地向后和向前调整。也就是说,可能会观察到时间跳变

要获取有关 Erlang 运行时系统 OS 系统时间来源的信息,请调用erlang:system_info(os_system_time_source)

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/0

计时器

所有计时器都是相对于 Erlang 单调时间触发的。目前,所有计时器在 API 和运行时系统内部都具有毫秒分辨率。也就是说,分辨率(以及精度和准确度)不会高于毫秒。如果Erlang 单调时间的分辨率低于毫秒,则计时器分辨率也将低于毫秒。

运行时系统启动以来,计时器只能在整数毫秒时触发。不允许计时器在用户给定的超时时间之前触发。也就是说,假设系统没有高负载,当用户给出的超时时间为 T 时,计时器通常会在 [T, T+1) 毫秒范围内触发。如果系统负载很高,则可能需要更长的时间才能触发计时器。

简介

时间对于 Erlang 程序至关重要,更重要的是,正确的时间对于 Erlang 程序至关重要。由于 Erlang 是一种具有软实时属性的语言,并且我们可以在程序中表达时间,因此虚拟机和语言必须谨慎对待什么是正确的时间以及时间函数的行为方式。

在设计 Erlang 时,假设系统中的挂钟时间显示单调时间,其前进速度与时间的定义完全相同。这或多或少意味着原子钟(或更好的时间源)预计会连接到您的硬件,然后该硬件预计会被永久锁定,免受任何人为干预。虽然这可能是一个引人入胜的想法,但事实并非如此。

“普通”的现代计算机无法自行保持时间,除非您有一个芯片级原子钟连接到它。您的计算机感知到的时间通常必须进行校正。因此,网络时间协议 (NTP) 协议以及 ntpd 进程会尽最大努力使您的计算机时间与正确的时间同步。在 NTP 校正之间,通常会使用比原子钟性能更差的时间保持器。

但是,NTP 并非万无一失。NTP 服务器可能不可用,ntp.conf 可能配置错误,或者您的计算机有时可能会断开与 Internet 的连接。此外,您可能有一个用户(甚至是系统管理员),他们认为处理夏令时的正确方法是每年两次将时钟调整一小时(这是不正确的方法)。更复杂的是,此用户从 Internet 获取了您的软件,并且没有考虑计算机感知到的正确时间是什么。用户并不关心使挂钟与正确的时间同步。用户希望您的程序拥有关于时间的无限知识。

大多数程序员也希望时间可靠,至少在他们意识到工作站上的挂钟时间差一分钟之前是这样的。然后,他们将其设置为正确的时间,但很可能不是以平滑的方式进行。

如果总是期望系统上的挂钟时间是正确的,可能会出现大量问题。因此,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 时间的时间。

    如果您不确定操作系统系统时间是否正确,请将其设置为一个保证早于实际 POSIX 时间的时间,以确保安全,然后再启动 Erlang 运行时系统。

  • 最终确定正确的操作系统系统时间 - 当用户最终确定时间偏移时,操作系统系统时间必须是正确的。

如果未满足这些要求,系统可能会表现得很糟糕。

假设满足这些要求,启用了时间校正,并且使用诸如 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 中那样实现。