【问题标题】:What happens when GetTickCount() wraps?GetTickCount() 换行时会发生什么?
【发布时间】:2009-04-07 22:59:07
【问题描述】:

如果一个线程正在做这样的事情:

const DWORD interval = 20000;
DWORD ticks = GetTickCount();
while(true)
{
    DoTasksThatTakeVariableTime();

    if( GetTickCount() - ticks > interval )
    {
        DoIntervalTasks();
        ticks = GetTickCount();
    }
}

最终,当值不适合 DWORD 时,滴答声将自动换行。

我一直在和一位同事讨论这个问题。我们中的一个人认为,当换行发生时,代码仍然会表现得“很好”,因为减法运算也会换行。我们中的另一个人认为它并不总是有效,尤其是在间隔很大的情况下。

谁是对的,为什么?

谢谢。

【问题讨论】:

  • 您真正要问的是,当您从一个小的 DWORD 中减去一个大的 DWORD 时会发生什么。您和您的同事本可以编写一个程序来找出答案。 GetTickCount 与此无关。
  • 我对 calc.exe 进行了实验,使用 dword 大小设置为十六进制模式。结果看起来它会表现得很好......但是,我的同事并不相信它会在所有情况下都有效,因此这个问题。 GetTickCount 是相关的,Jon Skeet 已经指出我们可能会使用 GetTickCount64
  • GetTickCount64() 只是将问题的规模更改为在大约 2000 亿天之后换行......你确定你的代码不会运行那么久吗? ;-)

标签: c++ c windows winapi


【解决方案1】:

来自the docs

经过的时间存储为 DWORD 价值。因此,时间将结束 如果系统运行,则大约为零 连续 49.7 天。避免 这个问题,使用GetTickCount64。 否则,检查溢出 比较时间时的条件。

但是,DWORD 是无符号的 - 所以你应该没问题。 0 - “非常大的数字” = “小数字”(当然,假设您没有任何溢出检查活动)。我之前的编辑建议你得到一个负数,但那是在我考虑到 DWORD 未签名之前。

如果手术需要不到 49.7 天,您仍然会遇到问题。这对你来说可能不是问题;)

一种测试方法是将GetTickCount() 方法存根,这样您就可以编写单元测试,明确地将其包装起来。再说一次,如果你真的只是怀疑算术部分,你可以很容易地为此编写单元测试:) 真的,只要你知道它的行为,这个数字来自系统时钟的事实几乎是无关紧要的wraps - 并且在文档中指定。

【讨论】:

    【解决方案2】:

    没有坏事发生,只要:

    • 你减去DWORDs,而不是先转换成其他类型。

    • 您尝试计时的时间不会超过 49.7 天。

    这是因为无符号算术溢出在 C 中定义良好,而包装行为正是我们想要的。

    DWORD t1, t2;
    DWORD difference;
    
    t1 = GetTickCount();
    DoSomethingTimeConsuming();
    t2 = GetTickCount();
    

    t2 - t1 将产生正确的值,即使 GetTickCount 环绕。在做减法之前不要将t2t1 转换为其他类型(例如intdouble)。

    如果编程语言将溢出视为错误,这将不起作用。如果DoSomethingTimeConsuming() 花费的时间超过 49.7 天,它也将不起作用。不幸的是,您无法仅通过查看 t2t1 来判断 GetTickCount 缠绕了多少次。


    让我们从通常的情况开始,没有回绕起作用:

    t1 = 13487231
    t2 = 13492843
    

    这里是t2 - t1 = 5612,这意味着操作大约需要五秒钟。

    现在考虑一个需要很短时间的操作,但 GetTickCount 确实环绕:

    t1 = 4294967173
    t2 = 1111
    

    该操作花费了 1234 毫秒,但计时器回绕,1111 - 4294967173-4294966062 的虚假值。我们会怎么做?

    嗯,模 232,减法的结果也环绕:

    (DWORD)-4294966062 == (DWORD)1234
    

    最后,考虑一个操作需要 几乎 232 毫秒但不完全是这样的边缘情况:

    t1 = 2339189280
    t2 = 2339167207
    

    在这里,GetTickCount 绕了一圈,然后又回到了原来的位置。

    现在t2 - t1 产生4294945223 的虚假值。那是因为这是手术实际花费的时间!

    一般:

    (base + offset) - base ≡ offset mod 2^32
    

    【讨论】:

      【解决方案3】:

      如果您想测试 GetTickCount() 包装时会发生什么,您可以启用 Application Verifier 的 TimeRollOver 测试。

      来自Using Application Verifier Within Your Software Development Lifecycle

      TimeRollOver 强制 GetTickCount 和 TimeGetTime API 以比通常更快的速度翻转。这使应用程序可以更轻松地测试其对时间翻转的处理。

      【讨论】:

        【解决方案4】:

        我建议计算两个刻度之间的实际经过时间,而不是依赖编译器为您处理它:

        const DWORD interval = 20000;
        
        #define TICKS_DIFF(prev, cur) ((cur) >= (prev)) ? ((cur)-(prev)) : ((0xFFFFFFFF-(prev))+1+(cur))
        
        DWORD ticks = GetTickCount();
        while(true)
        {
            DoTasksThatTakeVariableTime();
        
            DWORD curticks = GetTickCount();
            if( TICKS_DIFF(ticks, curticks) > interval )
            {
                DoIntervalTasks();
                ticks = GetTickCount();
            }
        }
        

        【讨论】:

          【解决方案5】:

          我最近遇到了这个问题。我正在处理的代码在很多地方使用 GetTickCount() 来确定程序是否在特定任务上花费了太多时间,如果是,它将使该任务超时并重新安排它以供以后执行。会发生的情况是,如果 GetTickCount() 在某个测量周期内包装,它会导致代码过早超时。这是一项持续运行的服务,因此每隔 49 天,它就会出现一次小故障。

          我通过编写一个在内部使用 GetTickCount() 的计时器类来修复它,但检测到值何时包装并补偿它。

          【讨论】:

            【解决方案6】:

            您可以对其进行测试;) - 我在这里有一个简单的测试应用程序,它将启动一个应用程序并在其中挂钩 GetTickCount(),以便您可以从测试应用程序的 GUI 控制它。我写它是因为在某些应用程序中删除 GetTickCount() 调用并不容易。

            TickShifter 是免费的,可在此处获得:http://www.lenholgate.com/blog/2006/04/tickshifter-v02.html

            我在写一系列关于测试驱动开发的文章时写了它,其中使用了一些以错误方式使用GetTickCount() 的代码。

            如果您有兴趣,可以在这里找到文章:http://www.lenholgate.com/blog/2004/05/practical-testing.html

            但是,总而言之,您的代码可以工作...

            【讨论】:

              【解决方案7】:

              这篇文章对我有帮助,但我认为有一个错误:

              #define TICKS_DIFF(prev, cur) ((cur) >= (prev)) ? ((cur)-(prev)) : ((0xFFFFFFFF-(prev))+(cur))
              

              当我在环绕点进行测试时,我发现它偏离了 1。

              对我有用的是:

              define TICKS_DIFF(prev, cur) ((cur) >= (prev)) ? ((cur)-(prev)) : ((0xFFFFFFFF-(prev))+(cur)+1)
              

              【讨论】:

              • 或者您是否尝试过更简单的等价物:TICKS_DIFF(prev,cur) ((cur)-(prev)) 它甚至可以使用环绕。
              【解决方案8】:

              您当然需要处理这个刻度线问题。

              Linux 内核通过以下技巧来处理此类滴答问题:

              #define time_after(a,b) ((long)(b) - (long)(a)

              想法是将unsigned转换为有符号并比较它们的值,那么只有|a-b|

              您可以试试这个技巧并了解它的工作原理。

              因为 DWORD 也是 unsigned int,所以这个技巧也应该适用于 windows。

              所以你的代码可能是这样的:

              常量 DWORD 间隔 = 20000;

              DWORD 刻度 = GetTickCount() + 间隔;

              而(真){

              DoTasksThatTakeVariableTime();
              
              if(time_after(ticks, GetTickCount())
              {
                  DoIntervalTasks();
                  ticks = GetTickCount() + interval;
              } 
              

              }

              只有间隔小于 0x2^30 才有效。

              【讨论】:

                【解决方案9】:

                我知道这在近 11 年后完全无关紧要,并且从 Vista 开始包含 GetTickCount64(),但这里有一些帮助代码,我从永远以来一直在我的 takeit 中使用,我曾经使用过这是一个问题。

                inline DWORD GetElapsed(DWORD from, DWORD to = ::GetTickCount())
                {
                  if (from < to)        //check for wrap around condition
                      return (to - from);
                  else
                      return ((0xFFFFFFFFL - from) + 1 + to);
                }
                

                用法

                DWORD start = ::GetTickCount();
                
                // Some time later
                
                DWORD elapsed = GetElapsed(start);
                

                担心的不是您的操作可能需要超过 49.7 天,而是滴答计数器可能会在您的操作期间翻转,从而使您的经过时间计算不可靠。

                当然,现在因为 GetTickCount64() 而一切都无关紧要了

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2010-09-26
                  • 2021-05-07
                  • 2012-09-10
                  • 2010-10-07
                  • 2015-09-14
                  • 2019-11-16
                  相关资源
                  最近更新 更多