65.9K
CodeProject 正在变化。 阅读更多。
Home

日期和时间源接口及其实现

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016 年 12 月 10 日

CPOL

6分钟阅读

viewsIcon

10705

downloadIcon

103

日期和时间源接口允许在您的 C# 代码中使用不同的实际日期和时间源(例如当前系统时间或加速系统时间),以简化调试、模拟等。

引言

我们的代码经常处理日期和时间。例如,我们可能需要在指定的日期和时间(比如每周一上午 10:00)执行一些工作;或者某个操作必须每 5 分钟重复一次。.NET 平台为此提供了有用的类型,包括 System.DateTimeSystem.TimeSpanSystem.Diagnostics.Stopwatch 以及不同的计时器类。但在某些情况下,我们需要 .NET “开箱即用”无法提供的功能。

假设我们的应用程序每天(或每小时,这无关紧要)按计划执行某个操作。如果我们需要调试应用程序,那么我们肯定不想等到操作时间“自然”到来。有几种方法可以帮助解决这种情况。如果应用程序逻辑由某些可编辑设置(例如,存储在配置文件中)控制,那么可以创建特殊的调试设置;或者我们可以将当前系统时间更改为“事件发生前”的某个时刻。本文描述了另一种选项:通过使用日期和时间源(或称为提供者)接口启用日期和时间“虚拟化”。本文介绍了我对这种接口的设想。还提出了该接口的两种实现。一种实现提供系统日期和时间;它是直接使用 .NET 类型(DateTimeStopwatch)的替代方案。另一种实现允许指定时间加速(减速)因子,以便使用它的代码的时间相对于系统时间实际上运行得更快或更慢。还有一种选项可以指定任意日期和时间值作为起点。此功能可用于在调试或开发某些模拟代码时重现依赖于时间的结果。

Using the Code

可下载存档内容

该存档包含 Visual Studio 2013 解决方案 DateTimeSource。日期和时间源接口 IDateTimeSource 及其两个实现 SystemDateTimeSourceSimulatedDateTimeSource 定义在同一个源文件 DateTimeSource.cs 中。该解决方案还包含两个测试项目:UnitTests 控制台测试程序和 TestSimDateTimeGui WinForms 应用程序。

IDateTimeSource 接口

获取当前日期和时间

该接口提供了一个方法,用于获取当前(就实现该接口的类而言)UTC 日期和时间,作为 DateTime 值。该值可能等于或不等于“实际”日期和时间(即当前系统时间),具体取决于接口实现的语义。使用 DateTime.ToLocalTime 方法将返回的值转换为本地时间。

public interface IDateTimeSource
{
    DateTime GetUtcNow();

如果您想从使用可自定义的日期和时间源中获益,那么在所有依赖于当前日期和时间的代码中,都应使用 GetUtcNow 方法,而不是 DateTime.NowDateTime.UtcNow,例如:

void DoSomethingAt10AM(IDateTimeSource dtSrc)
{
    TimeSpan localTime = dtSrc.GetUtcNow().ToLocalTime().TimeOfDay;
    if (localTime.Hours == 10)
    {
        // do some operation
    }
}

使用与刻度相关的方法

该接口提供了几种处理“刻度”的方法,可用于测量时间间隔。刻度以任意整数单位表示日期和时间。每秒刻度的确切数量(即赫兹频率)取决于接口实现。可以通过 GetFrequency 方法获取。

public interface IDateTimeSource
{
    long GetTicks();

    long GetFrequency();

刻度相关的方法可用于以指定的时间间隔重复某些操作。假设我们需要实现一个函数,该函数返回某个传感器(例如,温度计)的当前测量值。假设获取这些测量值是相对耗时的操作;同时,我们事先知道测量的参数不能变化太快。在这种情况下,我们可以通过缓存最后接收到的值并将其返回给调用者,直到该值过期来提高性能。

readonly TimeSpan minCheckInterval = TimeSpan.FromMilliseconds(500);

long lastCheckTicks = -1;

double lastValue;

double GetMeasurement(IDateTimeSource dtSrc, MySensor sensor)
{
    if ((lastCheckTicks < 0) || (dtSrc.TimeElapsedFrom(lastCheckTicks) >= minCheckInterval))
    {
       // we may need to save current ticks before or after calling to GetValue()
       // depending on the cache logic we want to implement
       lastCheckTicks = dtSrc.GetTicks();

       // a time-consuming operation
       lastValue = sensor.GetValue();
    }
    return lastValue;
}

借助 IsTimeElapsedFrom 方法,该函数可以变得更简单

double GetMeasurement(IDateTimeSource dtSrc, MySensor sensor)
{
    if (dtSrc.IsTimeElapsedFrom(ref lastCheckTicks, minCheckInterval))
    {
        lastValue = sensor.GetValue();
    }
    return lastValue;
}

有一个方法可以将刻度转换为 DateTime

public interface IDateTimeSource
{
    DateTime GetDateTimeUtc(long ticks);

运行此代码片段后...

DateTime dt1 = dtSrc.GetUtcNow(); // [1]
long ticks = dtSrc.GetTicks(); // [2]
DateTime dt2 = dtSrc.GetDateTimeUtc(ticks); // [3]

...我们可能会期望 dt2 通常几乎等于 dt1,即如果代码执行没有被中断相当长的时间(这在多任务环境中总是可能的),并且在执行行 [1] 之后但在 [2] 或 [3] 之前系统时间没有被校正。

SystemDateTimeSource

该类是基于当前系统时间的 IDateTimeSource 实现。

GetUtcNow 方法简单地返回 DateTime.UtcNow

GetTicks 和其他与刻度相关的方法基于 Stopwatch

public interface IDateTimeSource
{
    public long GetTicks()
    {
        return Stopwatch.GetTimestamp();
    }

无需创建 SystemDateTimeSource 的多个实例。StaticSystemDateTime 为其实现了单例模式。

DateTime utcNow = SystemDateTime.Instance.GetUtcNow();

SimulatedDateTimeSource

IDateTimeSource 实现展示了日期和时间虚拟化的真正强大功能。该类允许其虚拟“时钟”相对于系统时间加速/减速,并允许指定任意起始(初始)日期和时间。

使用时间加速

类构造函数允许指定时间加速因子以及可选的开始日期和时间。

public class SimulatedDateTimeSource : StopwatchDateTimeSource, IDateTimeSource
{
    public SimulatedDateTimeSource(double timeAcceleration, DateTime? start = null)

考虑以下代码片段

var dtSrc = new SimulatedDateTimeSource(2.0); // time acceleration factor is 2
DateTime dt1 = dtSrc.GetUtcNow();
DateTime dt2 = SystemDateTime.Instance.GetUtcNow();
TimeSpan diff = dt1 - dt2; // [1]
Console.WriteLine(diff);
Thread.Sleep(1000);
dt1 = dtSrc.GetUtcNow();
dt2 = SystemDateTime.Instance.GetUtcNow();
diff = dt1 - dt2; // [2]
Console.WriteLine(diff);

在行 [1] 之后,diff 的值通常会接近 TimeSpan.Zero(即 dt1dt2 几乎相等),与之前相同(代码执行未中断,系统时间未校正)。

在行 [2] 之后,diff 的值预计约为 +1 秒,因为此模拟日期/时间源实例内的“时钟”运行速度是实际系统时间的两倍。

设置开始日期和时间

我们也可以为我们的日期/时间源指定任意的开始日期和时间。

var startDt = new DateTime(1900, 1, 1, 12, 0, 0, DateTimeKind.Utc);
var dtSrc = new SimulatedDateTimeSource(1.0, startDt);
Console.WriteLine(dtSrc.GetUtcNow().ToString(CultureInfo.InvariantCulture));
Thread.Sleep(1000);
Console.WriteLine(dtSrc.GetUtcNow().ToString(CultureInfo.InvariantCulture));

预期输出

01/01/1900 12:00:00
01/01/1900 12:00:01

还有一个第二个构造函数,允许指定另一个 IDateTimeSource 作为此 SimulatedDateTimeSource 的基础。这使得创建“链式”日期/时间源成为可能。

public class SimulatedDateTimeSource : StopwatchDateTimeSource, IDateTimeSource
{
    public SimulatedDateTimeSource
      (IDateTimeSource source, double timeAcceleration, DateTime? start = null)

限制

如果使用加速,日期/时间分辨率会降低

当前的 SimulatedDateTimeSource 实现默认使用 SystemDateTimeSource 作为基本日期/时间源;反过来,SystemDateTimeSource 在其 GetUtcNow 中返回 DateTime.UtcNowDateTime.UtcNow (DateTime.Now) 的分辨率不高。我在我的系统上执行了此代码以找到它。

DateTime dt2;
int count = 0;
var dt1 = DateTime.UtcNow;
do
{
    dt2 = DateTime.UtcNow;
    ++count;
}
while (dt1 == dt2);
Console.WriteLine("Count: {0}. Diff: {1}", count, (dt2 - dt1));

观察到的典型结果

Count: 128419. Diff: 00:00:00.0050000
Count: 136151. Diff: 00:00:00.0050000
Count: 137519. Diff: 00:00:00.0050001

发现的日期/时间分辨率约为 5 毫秒。

这是 SimulatedDateTimeSourceGetUtcNow 实现

public DateTime GetUtcNow()
{
    TimeSpan diff = source.GetUtcNow() - startDateTime;
    double ticks = (double)startDateTime.Ticks + correction.Ticks + (diff.Ticks * acceleration);
    if (ticks >= DateTime.MaxValue.Ticks)
    {
        return DateTime.MaxValue;
    }
    if (ticks <= DateTime.MinValue.Ticks)
    {
        return DateTime.MinValue;
    }
    return new DateTime((long)ticks, DateTimeKind.Utc);
}

您会看到时间差刻度乘以 acceleration,这会使分辨率变差。我运行了修改后的测试代码。

var dtSrc = new SimulatedDateTimeSource(3.0);
DateTime dt2;
int count = 0;
var dt1 = dtSrc.GetUtcNow();
do
{
    dt2 = dtSrc.GetUtcNow();
    ++count;
}
while (dt1 == dt2);
Console.WriteLine("Count: {0}. Diff: {1}", count, (dt2 - dt1));

典型结果

Count: 52274. Diff: 00:00:00.0150016
Count: 46914. Diff: 00:00:00.0149888
Count: 46679. Diff: 00:00:00.0150016

现在日期/时间分辨率约为 15 毫秒(差了 3 倍,与 acceleration 成正比)。

时间加速因子限制

当前 SimulatedDateTimeSource 实现不接受时间加速因子,如果结果频率不符合 [1..long.MaxValue] 范围。下面是 SimulatedDateTimeSource.Reset 方法的一部分,该方法由两个 SimulatedDateTimeSource 构造函数使用,也可以在构建类的实例后随时调用以更改模拟参数。

public void Reset(IDateTimeSource source, double timeAcceleration, DateTime? start = null)
{
    double f = source.GetFrequency() * timeAcceleration;
    if ((f > long.MaxValue) || (f < 1))
    {
        throw new ArgumentOutOfRangeException("timeAcceleration");
    }

另一个重要的事实是,每次 SystemDateTimeSource 实例将刻度转换为 TimeSpan(这发生在 GetTimeSpanTimeElapsedFromIsTimeElapsedFromGetDateTimeUtc 方法中)时,它都会将结果裁剪到 [TimeSpan.MinValue..TimeSpan.MaxValue] 范围。当单独使用 SystemDateTimeSource 时,这并不重要,但如果使用具有较大时间加速因子的 SimulatedDateTimeSource(默认基于 SystemDateTimeSource),则应将其考虑在内:在这种情况下,实际上可以达到 TimeSpan.MaxValue 值。

关注点

处理日期和时间以及测量时间间隔并不像看起来那么容易。许多因素使其复杂化:硬件限制(例如,系统实时时钟漂移)、CPU 节电模式、多任务效应、多处理器/多核效应、非平凡的系统时间同步算法。CodeProject 上有一些有趣的文章涵盖了这些主题,例如 双计时器故事

历史

  • 2016 年 12 月 12 日:首次修订
© . All rights reserved.