DateTime.Now更新的频率有多高?还是有更精确的API来获取当前时间?

29

我有一段在循环中运行的代码,它根据当前时间保存状态。有时这只间隔了几毫秒,但由于某种原因,即使它只晚了2或3毫秒,DateTime.Now似乎总是返回至少相差10毫秒的值。这是一个重大问题,因为我保存的状态取决于保存时的时间(例如,记录某些内容)。

我的测试代码每隔10毫秒返回一个值:

public static void Main()
{
    var dt1 = DateTime.Now;
    System.Threading.Thread.Sleep(2);
    var dt2 = DateTime.Now;

    // On my machine the values will be at least 10 ms apart
    Console.WriteLine("First: {0}, Second: {1}", dt1.Millisecond, dt2.Millisecond);
}

有没有其他方法可以获取毫秒级别精确的当前时间?

有人建议查看Stopwatch类。虽然Stopwatch类非常准确,但它并不告诉我当前时间,而这是我保存程序状态所需要的。


1
来这里的人,TL;DR http://msdn.microsoft.com/en-us/library/ms644904%28VS.85%29.aspx - AnotherUser
7个回答

43

有趣的是,你的代码在我的四核Win7上完美地运行,几乎每次都生成恰好相隔2毫秒的值。

因此,我进行了更彻底的测试。这是我用Thread.Sleep(1)进行的示例输出。代码在循环中打印连续调用DateTime.UtcNow之间的毫秒数:

sleep 1

每行包含100个字符,因此代表“干净运行”上的100ms时间。因此,此屏幕涵盖大约2秒钟。最长的抢占时间为4ms;而且,在持续约1秒钟的时间内,每次迭代都需要精确1ms。那几乎就像实时操作系统的质量!1 :)

于是我再试一次,这次使用Thread.Sleep(2)

sleep 2

再次得到几乎完美的结果。这次每行长200ms,并且有一次近3秒钟的运行,其中缝隙永远不会超过2ms。

自然,下一步需要看的是我的机器上DateTime.UtcNow的实际分辨率。这里没有任何睡眠的运行;如果UtcNow根本没有改变,则会打印“。”:

no sleep

最后,在调查在同一台机器上时间戳相隔15ms的奇怪情况时,我遇到了以下有趣的情况:

enter image description hereenter image description here

Windows API中有一个名为timeBeginPeriod的函数,应用程序可以使用它来临时增加计时器频率,因此这可能是发生的情况。计时器分辨率的详细文档可通过硬件开发中心档案,特别是Timer-Resolution.docx(Word文件)进行查看。

结论:

  • DateTime.UtcNow的分辨率可能比15ms高得多
  • Thread.Sleep(1)可能确实会睡眠1ms
  • 在我的机器上,UtcNow每次增加精确1ms时间(除去一个舍入误差-反编译器显示UtcNow中有个除法运算)。
  • 进程可能在低分辨率模式和一切都是基于15.6ms的高分辨率模式之间进行切换。

以下是代码:

static void Main(string[] args)
{
    Console.BufferWidth = Console.WindowWidth = 100;
    Console.WindowHeight = 20;
    long lastticks = 0;
    while (true)
    {
        long diff = DateTime.UtcNow.Ticks - lastticks;
        if (diff == 0)
            Console.Write(".");
        else
            switch (diff)
            {
                case 10000: case 10001: case 10002: Console.ForegroundColor=ConsoleColor.Red; Console.Write("1"); break;
                case 20000: case 20001: case 20002: Console.ForegroundColor=ConsoleColor.Green; Console.Write("2"); break;
                case 30000: case 30001: case 30002: Console.ForegroundColor=ConsoleColor.Yellow; Console.Write("3"); break;
                default: Console.Write("[{0:0.###}]", diff / 10000.0); break;
            }
        Console.ForegroundColor = ConsoleColor.Gray;
        lastticks += diff;
    }
}

事实证明存在一个未记录的函数可以改变计时器分辨率。我还没有详细调查,但我想在这里发布一个链接:NtSetTimerResolution

1当然,我确保操作系统尽可能处于空闲状态,并且它有四个相当强大的CPU核心可供使用。如果我将所有四个核心负载到100%,情况就完全不同了,到处都是长时间的抢占。


5
那最后的两句话相当重要 :-) - nos
3
有记录的函数可以改变计时器分辨率。这些方法是 timePeriodBegintimePeriodEnd - Chuu

10

处理毫秒级时间时,DateTime类并不是问题的根源,而与CPU滴答和线程切片有关。实际上,当操作被调度器暂停以允许其他线程执行时,它必须等待至少1个时间片才能恢复执行,而在现代Windows操作系统中,这个时间片大约是15毫秒。因此,任何试图暂停少于这个15毫秒精度的尝试都将导致意外结果。


4
问题出在 DateTime.Now 的精度只有10毫秒...请参考 http://msdn.microsoft.com/zh-cn/library/system.datetime.now.aspx - configurator
8
你认为这个10毫秒的限制是从哪里来的?提示:这不是随意确定的。 - Chris

2

如果在进行任何操作之前,您先记录下当前时间的快照,那么您只需将计时器的时间加到您存储的时间上,是吗?


有趣的想法。这是高性能关键代码,我不确定使用性能计数器(Stopwatch内部API)是否是一个好主意。我会去检查一下。 - Brian Leahy
我在我的基准测试中发现(针对一个更大的系统),在一百万次调用中,使用DateTime.UtcNow和将Stopwatch值添加到公共基础时间之间没有明显的区别。 - ygoe

1

你应该问自己是否真的需要精确的时间,还是仅需要足够接近的时间加上一个递增的整数。

通过在等待事件(如互斥、选择、轮询、WaitFor*等)之后立即获取now(),然后添加一个序列号,可以做出好事情,可能是在纳秒范围内或任何有空间的地方。

您还可以使用rdtsc机器指令(一些库提供了API包装器,不确定在C#或Java中是否可以这样做)从CPU获取廉价时间,并将其与now()的时间结合使用。rdtsc的问题在于,在速度缩放系统上,您永远无法确定它会做什么。它也会相当快地环绕。


我的问题是我需要重放保存的状态。因此,如果我保存了两个状态,相隔5毫秒,我需要能够以5毫秒的间隔重放它们。仅仅知道顺序是不够的。 - Chris Jester-Young
我不知道你试图解决的问题是什么,但你可能过于努力了。只要在10毫秒的精度内正确排序可能就足够了。 - Zan Lynx
或者听起来像是一个需要硬件加速或在实时环境中实现的任务。 - binki

0
回答你关于更精确API的问题的第二部分,AnotherUser的评论引导我找到了这个解决方案,在我的情况下克服了DateTime.Now精度问题:
static FileTime time;        

public static DateTime Now()
{
    GetSystemTimePreciseAsFileTime(out time);
    var newTime = (ulong)time.dwHighDateTime << (8 * 4) | time.dwLowDateTime;
    var newTimeSigned = Convert.ToInt64(newTime);
    return new DateTime(newTimeSigned).AddYears(1600).ToLocalTime();
}        

public struct FileTime
{
    public uint dwLowDateTime;
    public uint dwHighDateTime;
}

[DllImport("Kernel32.dll")]
public static extern void GetSystemTimePreciseAsFileTime(out FileTime lpSystemTimeAsFileTime);

在我的基准测试中,迭代100万次,平均返回3个刻度,而DateTime.Now只有2个刻度。

为什么1600超出了我的权限范围,但我使用它来获取正确的年份。

编辑:这在win10上仍然是一个问题。任何感兴趣的人都可以运行这个证据片段:

void Main()
{
   for (int i = 0; i < 100; i++)
   {        
       Console.WriteLine(Now().ToString("yyyy-MM-dd HH:mm:ss.fffffff"));
       Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fffffff"));
       Console.WriteLine();
   }    
}
// include the code above

0
我用来完成这项任务100%准确的所有工具只有一个计时器控件和一个标签。
代码并不需要太多解释,相当简单。 全局变量:
int timer = 0;

这是 tick 事件:

private void timeOfDay_Tick(object sender, EventArgs e)
    {

        timeOfDay.Enabled = false;
        timer++;

        if (timer <= 1)
        {
            timeOfDay.Interval = 1000;
            timeOfDay.Enabled = true;             
            lblTime.Text = "Time: " + DateTime.Now.ToString("h:mm:ss tt");                  
            timer = 0;
        }


}

这是表单加载:
private void DriverAssignment_Load(object sender, EventArgs e)
    {


        timeOfDay.Interval= 1;
        timeOfDay.Enabled = true;


}

-4
你可以使用 DateTime.Now.Ticks,阅读 MSDN 上的文章。
“一个 tick 表示一百纳秒或十分之一微秒。”

看起来很有前途,我会去看看。 - Chris Jester-Young
7
你认为这些滴答声来自哪里?即使你用不准确的滴答声测量时间,你仍然会遇到时间延迟。所有的滴答声属性都是对日期时间值的滴答声表示,但你仍然会遇到精度问题。请问需要翻译的内容结束了吗? - RhysC
问题在于Ticks提供了误导性的精度。如上所述,DateTime.Now属性及其Ticks仅每10毫秒更新一次。 - Spoc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接