什么是.NET应用程序最稳定的时间戳源?

时间:2013-06-12 23:50:19

标签: c# datetime

基础问题

我的C#应用​​程序中存在时间戳问题。我异步地从远程TCP连接接收数据。每次收到数据时,我都会将时间戳变量更新为DateTime.Now。在一个单独的线程上,每秒一次,我检查它是否超过自我上次收到后的预定义超时时间,如果是,则断开连接。这种方法已经工作了很多年,但现在我的情况是应用程序安装在具有不稳定时间源的机器上。每隔几天,机器时间"自动纠正"我提前断开连接。代码基本如下:

接收流程

void OnReceiveComplete(IAsyncResult ar) {
    ...
    mySocket.EndReceive(ar);
    lastRxTime = DateTime.Now;
    ...
}

检查流程

void CheckConnection() {
    TimeSpan ts = DateTime.Now.Subtract(lastRxTime);
    if(ts.TotalMilliseconds > timeout) {
        Disconnect(string.Format("No packet received from the server for over {0} seconds.", timeout / 1000));
    }
}

我在问题发生期间有有效的Wireshark捕获,并且在断开连接之前我看到NTP流量最终看起来像是至少1分钟的修正。这显然会导致检查过程失败。

技术细节/预期问题的答案

  1. 我可以控制连接的两端,但不能控制它们之间的物理层(通常是低质量的卫星链路),这就是为什么要进行超时检查。
  2. 由于数据是异步的,如果没有数据从服务器发送一段时间等于超时的一半,则可以发送一个小心跳。
  3. 此过程可能有多个实例(即在连接到多个不同服务器的计算机上。
  4. 所有通信都使用异步方法(据称使用完成端口)。
  5. 检查过程在单个线程上运行,该线程由单个计算机上的所有客户端共享。
  6. 完成研究(即可能的解决方案)

    我的谷歌搜索到目前为止导致以下结果:

    1. 我发现出于性能原因,我应该使用DateTime.UtcNow代替DateTime.Now。这不会影响问题本身。
    2. 依赖于Ticks的实现将是更好的解决方案。
    3. 获取刻度线有两种选择 - Environment.TickCountStopwatch.GetTimestamp()
    4. 根据我的研究,Environment.TickCount可能会受到时间调整的影响,但我不确定在什么情况下。此外,由于我在其他更高性能的情况下使用相同的方法,10-16毫秒的分辨率可能是一个问题(虽然不是我在这里提出的特定情况)。
    5. 当高性能时钟不可用时,Stopwatch.GetTimestamp()可以回退到DateTime.Now.Ticks。我不确定这种情况会发生多久(任何机器都不再配备高性能时钟),但我确信如果它转向Ticks,也会出现同样的问题。
    6. 我还读到Stopwatch.GetTimestamp()将使用QueryPerformanceCounter() API调用,并且从多个线程调用时可能会不稳定。
    7. 终极问题

      我很好奇生成lastRxTime时间戳的最佳方法是什么?我是否担心Environment.TickCountStopwatch.GetTimestamp()函数中的低可能性问题?只要他们考虑到应用程序的多线程特性以及链接质量问题,我就会对其他实现持开放态度。

      更新7/17/2013(已部署解决方案!)

      我已经部署了一个解决方案,并希望让所有人了解详细信息。一般来说,可能没有一个可接受的答案,但在经历了这个经历之后,我可以说最初的解决方案肯定是一个问题。我会尽量提供尽可能详细的信息:

      首先,NTP问题实际上是一个不同问题的症状。出现此问题的网络是AD Domain,其中两台服务器运行我的代码设置为域控制器。事实证明,DC是域的时间源。事实证明,系统时间从这些系统上的实时时钟漂移了大约11天的一分钟,此时Windows正在纠正滑动。一旦它纠正了第一个DC上的滑动,第二个DC就会同步他的时间并且都会遇到上述问题。

      根据反馈和我的原始研究,我创建了一个测试程序,用于在断开DateTime.Now,Environment.TickCount和Stopwatch.GetTimestamp()的日志值时运行。我发现在校正过程中,Environment.TickCount和StopWatch.GetTimeStamp()都没有滑落,这意味着它们很适合用作DateTime.Now()的替代品。我选择了TickCount,因为它保证在我所有已部署的服务器上(而秒表可以回退到我尚未找到的某些机器上的DateTime对象)。它到目前为止没有问题。我在滚动问题上做了尽职调查,以防止这种形式出现问题,但需要等待我的系统长时间运行。

      我想要注意的是,如果其他人遇到类似的问题,他们不应该在下面的列表中使用任何其他提供的解决方案。每个都有自己的优点,事实上简单的计数器可能是大多数情况下的最佳解决方案。我没有采用这种解决方案的原因是我在单独的区域中有类似的代码,严重依赖于紧密的时间。我可以处理那里的滴答计数的16毫秒左右的分辨率,但无法处理计数器解决方案产生的时间漂移​​(我在一个单独的产品中使用了这样的代码,这个产品每小时漂移超过一秒钟并带给我超出项目规范)。

      再次感谢所有人,如果再有任何问题,我一定会更新问题。

3 个答案:

答案 0 :(得分:3)

我认为使用Environment.TickCount没有任何问题。文档说明它返回:

  

一个32位有符号整数,包含自上次启动计算机以来经过的时间量(以毫秒为单位)。

接着说:

  

此属性的值是派生自系统计时器,并存储为32位有符号整数。

我没有看到NTP时间校正如何影响这一点。你有其他暗示的链接吗?棘手的部分是处理环绕。因此,在将当前的滴答计数与之前的滴答计数进行比较时,如果得到负数,则会知道发生回转。

就我个人而言,我认为这是一种优越的机制,因为返回值是毫秒级的,所以超时的转换因子很简单。否则,使用Stopwatch.GetTimestamp,您将需要做一些额外的工作来计算滴答频率。

此外,我假设这是通过调用Win32 GetTickCount函数在内部实现的。 (dotPeek显示它标有MethodImplOptions.InternalCall,所以我无法确定)但是该文档的文档说明:

  

<强>说明

     

GetTickCount功能的分辨率仅限于   系统计时器的分辨率,通常在10的范围内   毫秒到16毫秒。 GetTickCount的分辨率   功能不受调整的影响   GetSystemTimeAdjustment功能。


第二个想法,因为你只是每隔一秒定期检查一次,所以定时器的分辨率只需要&lt; 1秒。尝试做得更好是没有意义的,那么为什么要使用任何API调用呢?

private int m_conTimer;

void OneSecondThreadCallback() {
    if (++m_conTimer >= TIMEOUT_VALUE)
        // Connection timed out. React accordingly.
}

答案 1 :(得分:3)

有一些很好的理由不使用DateTime.Now来实现此目的

  • 正如您所指出的,存在性能问题。主要是因为它内部必须转换为当地时区。

  • 由于它是本地时区的“现在”,因此不适合进行比较或计算。这是因为由于夏令时转换发生,许多时区每年有两次不连续。如果您要比较上一次的活动时间,那么当时钟前进时,您将需要额外的一小时。当它们回滚时,您可以在0到60分钟之间离开(具体取决于您进行比较的时间)。

  • 它的精确度远低于StopwatchEnvironment.TickCount。虽然DateTime能够表示非常少的时间,但系统时钟本身仅精确到大约10到30毫秒。 (但也许这对你的用例来说并不那么重要。)

使用DateTime.UtcNow确实解决了前两点,但不是第三点。为此,您需要完全远离DateTime

此外 - 还有“时钟漂移”的概念。时钟可能会在较长时间内减速或加速。如果操作系统有一个时间同步源,它实际上可以调整实际传递的时间量以便进行补偿。您可以在this article中阅读更多内容(请参阅“时间调整”部分)。

您可能还对ClockRes实用程序感兴趣,可以使用它来为您提供有关系统计时器分辨率的信息。

- 您的用例最好不是通过任何这些技术提供的。你说你有兴趣在超时后取消一个事件。您还说过,您有一个单独的线程,它会定期检查超时时间是否已经过去。所有这一切都是不必要的。相反,只需使用Timer类。

请注意,.Net中有three different timers个版本。您可能需要System.Threading.Timer

// start the timer, callback in 10000 milliseconds, and don't fire more than once
var timer = new Timer(TimerCallback, null, 10000, Timeout.Infinite);

// to reset the timer when you receive data
timer.Change(10000, Timeout.Infinite);

// to stop the timer completely
timer.Change(Timeout.Infinite, Timeout.Infinite);

// and in your callback
private void TimerCallback(object state)
{
    // disconnect, or do whatever you want
}

如果可能你会从多个线程重置计时器,那么你应该使用System.Timers.Timer - 这是线程安全的。

// to set up the timer
var timer = new Timer(10000) {AutoReset = false};
timer.Elapsed += TimerOnElapsed;

// to start the timer running
timer.Start();

// to reset the timer
timer.Stop();
timer.Start();

// and the callback
private void TimerOnElapsed(object sender, ElapsedEventArgs args)
{
    // disconnect, or do whatever you want
}

答案 2 :(得分:2)

  

我是否过多担心Environment.TickCount和Stopwatch.GetTimestamp()函数中的低可能性问题?

可能,是的。

我更喜欢Stopwatch.GetTimestamp()选项。如果系统上可用,则会使用高性能计时器,如果不是选项,则会使用回退到DateTime.Ticks。因此,您可以在给定硬件上获得最佳时序,并且当高性能计时器不可用时,这是一个很好的选择。

虽然Environment.TickCount可能是一个有趣的选项,但如果您的机器可能运行超过24.9天,则需要处理溢出的情况。