Home / ぼやきごと / 2015-11-11
2015-11-11

VC++の sleep_for 関数の実装がよろしくない

Visual C++ 2012, 2013, 2015 の std::this_thread::sleep_for 関数は、内部で std::this_thread::sleep_until 関数を呼び出す実装になっています。
具体的に言うと、 sleep_for の引数に渡された時間値を現在時刻に加算することで絶対時刻に変換し、それを sleep_until 関数の引数に渡しています。*1

この実装方法には問題があって、 sleep_for 関数の処理途中に別スレッドや別プロセスによって時刻が変更されると正常に動作しません。
例えば現在のPC時刻が 10時0分0秒 で、 sleep_for 関数の引数に 1秒 を渡し、その処理途中にユーザがPC時刻を 9時0分0秒 に変更したとすると、次のように動作します。

  1. sleep_for 関数内で現在のPC時刻 10時0分0秒 と引数値 1秒 を加算して絶対時刻 10時0分1秒 を作成。
  2. ここでユーザがPC時刻を 9時0分0秒 に変更。
  3. sleep_until 関数には 10時0分1秒 が渡される。
  4. PC時刻が 10時0分1秒 になるまで、 1時間0分1秒 の間スリープすることになってしまう。

実際にこのような挙動になることを確認するためのサンプルコードは下記の通りです。
なお、 SetSystemTime 関数を成功させるために管理者権限で実行する必要があります。*2

すべて開くすべて閉じる
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 
 
 
 
 
 
 
 
 
-
-
|
!
|
|
|
-
-
!
|
|
-
!
|
-
!
-
-
!
|
|
-
|
!
|
|
-
-
!
|
!
|
-
-
!
|
!
|
-
!
!
!
|
-
!
-
-
!
|
-
|
!
!
|
|
!
#include <thread>
#include <future>
#include <iostream>
 
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
 
int main()
{
    // 時刻変更スレッド実行
    // 100ミリ秒おきに1年進んだり戻ったりする
    auto f =
        std::async(
            std::launch::async,
            []
            {
                // 現在時刻取得
                ::SYSTEMTIME now = { };
                ::GetSystemTime(&now);
 
                // 挙動をわかりやすくするため最初に1秒待機
                ::Sleep(1000);
 
                // 100ミリ秒スパンでループ
                for (; ; ::Sleep(100))
                {
                    // 現在のPC時刻取得
                    ::SYSTEMTIME t = { };
                    ::GetSystemTime(&t);
 
                    // 本来の現在時刻より1年以上進んでいたら1年減算
                    // そうでなければ1年加算
                    if (
                        (t.wYear > now.wYear && t.wMonth >= now.wMonth) ||
                        (t.wYear > now.wYear + 1))
                    {
                        // 1年減算してから新しい現在時刻とする
                        --t.wYear;
                        now = t;
                    }
                    else
                    {
                        // 新しい現在時刻としてから1年加算する
                        now = t;
                        ++t.wYear;
                    }
 
                    // PC時刻変更
                    ::SetSystemTime(&t);
                }
            });
 
    // メインスレッドのループ
    for (unsigned long long i = 0; ; ++i)
    {
        // ループカウンタ表示
        std::cout << i << std::endl;
 
        // sleep_for によって10ミリ秒スリープ
        // ★VC++だとここで1年間スリープしてしまう場合がある。
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
 
    return 0;
}

コード内のコメントにも書いた通り、このサンプルでは1年間スリープすることになってしまい、プログラムが実質フリーズしてしまいます。

プログラム内では別スレッドからの時刻変更をしないように注意すればいいとしても、ユーザ操作による時刻設定を抑制するのは厳しいです。
当面は、素直に Windows API の Sleep 関数等を使っておくのが無難かもしれません。

なお、gccで使われているlibstdc++や、clang/LLVMで使われているlibc++における sleep_for 関数は、引数の時間値を時刻に変換したりせずそのままシステムのスリープ関数*3に渡す作りになっているため、VC++のような問題は起きません。
sleep_until 関数を直接呼び出した場合は同等の問題が起きる可能性がありますが、この関数は元々特定時刻まで待機するための関数なので、用途外のことに使わない限りはただ単に書いた通りに動くというだけの話でしょう。

Category: [C++][Visual Studio][プログラミング] - 2015-11-11 23:28:18

*1 より正確に言えばVC++独自実装の stdext::threads::xtime 型ポインタを引数に取る sleep_until 関数なんですが、処理内容としては同じことです。
*2 例えば Windows 10 ならば実行ファイルを右クリックして「管理者として実行」を選択する。
*3 nanosleep 関数等。