2017年6月1日木曜日

GetTickCountと49.7日問題にハマった

1秒ごとに実行させたいといった場合

【普通に思いつくコード】
DWORD tkNext = ::GetTickCount()+1000;    // 1秒後の時間を計算しておく

while (bRunning) {
::Sleep(1);    // Sleep(0)問題参照
if(::GetTickCount() <= tkNext){
continue;    // 1秒経って無ければcontinue
}
tkNext = ::GetTickCount()+1000;    // 1秒経ってるので、次の1秒を計算しておく
// なんらかの処理をする
}

これね、何の問題もなさそうに見えたんですわ・・・

しかし、下部のtkNext = ::GetTickCount()+1000のところで、
::GetTickCount()がDWORDの限界付近で、tkNextが0xffffffff付近になったとする
ループの頭に戻って、GetTickCountした時にDWORDを一周りして、ゼロ以上になっていたら、
(::Sleep(1)は10ミリ秒以上になるので)
::GetTickount() <= tkNextの所が、
if(10 <= 0xffffffff)になってしまう
すると、その後、ずっとcontinueしてしまうので、処理が行われなくなる。
49.7日後に再びGetTickCountがゼロになり、処理まで進まない。
(0xffffffff/1000/60/60/24 = 約49.7日)
このプログラムはおよそ49.7日で動かなくなる可能性があるのだ。

【修正版】
DWORD tkPrev = GetTickCount();   // 以前実行した時刻を保存しておく
while (bRunning) {
::Sleep(1);
if(::GetTickCount() - tkPrev < 1000){
continue;    // 前回実行時刻との差が1秒経って無ければcontinue
}
        tkPrev = ::GetTickCount();    // 1秒経ったので、現時刻を保存しとく
// なんらかの処理をする
}

こうやって、::GetTickCount() - tkPrev で判断すれば、DWORDを一回りするタイミングでも問題が無い
このコードは実はMSDNのGetTickCountでサンプルとして示されたコードとほぼ同一だ。
このコードの肝は符号拡張の部分なので、怪しいなと思ったらDWORD変数を作ってループさせるプログラムでも作って検証してみると良い。

0 件のコメント: