当GetTickCount()发生包装时会发生什么?

15

如果一个线程正在执行以下操作:

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

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

最终,当值无法适应DWORD时,ticks将会被包装。

我一直在与同事讨论这个问题。我们中的一个人认为,当发生包装时,代码仍将表现得“好”,因为减法操作也将进行包装。另一个人则认为,它不总是有效,特别是如果间隔很大。

谁是对的,为什么?

谢谢。


2
你真正想问的是从一个小的DWORD中减去一个大的DWORD会发生什么。你和你的同事可以编写一个程序来找出答案。GetTickCount与此无关。 - Rob Kennedy
我尝试了使用 dword 大小将 calc.exe 设置为十六进制模式的实验。结果看起来它会表现得很好... 然而,我的同事并不相信它在所有情况下都能正常工作,因此提出了问题。GetTickCount 是相关的,Jon Skeet 已经指出我们可能应该使用 GetTickCount64。 - Scott Langham
3
GetTickCount64() 只是改变了问题的尺度,大约在 2000 亿天后会发生循环问题... 你确定你的代码不会运行那么久吗?;-) - RBerteig
9个回答

13

根据文档

已过时间被存储为DWORD类型。因此,如果系统一直运行达到49.7天,则时间会重新从零开始计算。要避免这个问题,请使用GetTickCount64。否则,在比较时间时,请检查是否存在溢出条件。

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

不过,如果操作持续时间接近49.7天,你仍然会遇到问题。不过这可能对你来说不是问题;)

测试的一种方法是将 GetTickCount()方法拦截,以便您可以编写单元测试来明确使其循环。另外,如果您只是怀疑算术部分,也可以轻松地为其编写单元测试。实际上,从系统时钟获取数字的事实几乎无关紧要,只要您知道它在包装时的行为方式 - 这在文档中有说明。


注意:49.7天很可能会发生溢出。即4294967295毫秒为49.7102696181天。 - Sancarn

12

只要满足以下两点,就不会出错:

  • 先减去 DWORD 后,再进行其他类型的转换。

  • 计时的时间不超过 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
操作耗时1234ms,但计时器已经循环了,1111-4294967173是虚假值-4294966062。我们该怎么办呢?
好吧,在模2的32次方下,减法的结果也会循环:
(DWORD)-4294966062 == (DWORD)1234

最后,考虑一种极端情况,即操作所需时间接近于 232 毫秒,但并非完全相等:

t1 = 2339189280
t2 = 2339167207

在这里,GetTickCount 被包裹起来,然后又回到了原来的位置。

现在 t2 - t1 给出了一个看起来很假的值,4294945223。这是因为那就是操作实际花费的时间!

一般而言:

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

9
如果您想测试GetTickCount()溢出时会发生什么,您可以启用应用程序验证器的TimeRollOver测试。
来自在软件开发生命周期中使用应用程序验证器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();
    }
}

2
我最近遇到了这个问题。我正在处理的代码在许多地方使用GetTickCount()来确定程序是否花费了太多时间在特定任务上,如果是的话,它将超时该任务并重新安排它以供稍后执行。问题是,如果GetTickCount()在一个测量周期内发生了包装,它会导致代码过早超时。这是一个持续运行的服务,因此每49天,它都会有轻微的故障。
我通过编写一个计时器类来解决这个问题,该类在内部使用GetTickCount(),但检测值何时包装并进行补偿。

1

这篇文章对我很有帮助,但我认为其中存在一个错误:

#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)) 它甚至可以处理环绕情况。 - Scott Langham

1

你需要解决这个时钟溢出的问题。

Linux内核使用以下技巧来处理这种时钟溢出问题:

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

这个想法是将无符号数强制转换为有符号数并比较它们的值,只有当|a-b|<2^30时,才不会影响结果。

你可以尝试使用这个技巧,并了解它为什么有效。

由于DWORD也是无符号整数,所以这个技巧在Windows上也适用。

因此,你的代码可能类似于:

const DWORD interval = 20000;

DWORD ticks = GetTickCount() + interval;

while(true) {

DoTasksThatTakeVariableTime();

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

}

只有当间隔小于0x2^30时,它才有效。


1

你可以测试它 ;) - 我这里有一个简单的测试应用程序,它将启动一个应用程序并在其中挂钩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

然而,总之,您的代码将会工作...


1

我知道这完全与现在的情况无关,因为自Vista以来就包含了GetTickCount64(),但是这里有一些帮助代码,我从很久以前开始使用,当时这是一个问题。

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()的存在,这些都已经不再相关了。


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