タイムアウトの実装 - ElapsedTickTime

制御ソフトウェアでは、「タイムアウト」を実装することがよくあると思います。

今時は、async/await等を用いた非同期プログラミングが主流ではありますが、タイムアウト時間を厳密に管理したい場合などには、昔ながらのループ内でタイムアウトを待つ同期的な実装を行う場合も多々あると思います。

パッと思いつく方法はこんなコードかと。

DateTime startTime = DateTime.Now;

while(True) 
{
    if ((DateTime.Now - startTime).TotalMilliseconds >= 1000)
    {
        // timeout
        break;
    }
 
    // 何らかの処理
    {
    }
}

ただ、このコードだと待機中(Whileループ内)に、NTP等の時刻補正で時刻がマイナス(戻る)方向に補正された場合に意図しないタイムアウトが発生してしまいます。(不規則にタイムアウトが発生するので、原因特定に結構悩まされたりします)

これを回避するために、時刻補正に影響されない"Tick"値を使用する方法が考えられますが、"GetTickCount()"だと32bit値のため(4,294,967,295ms)待機中にラップアラウンドすると、誤動作の要因になります。(ラップアラウンドに影響を受けない実装方法もありますが)
 4,294,967,295 / 1000(ms) / 60(sec) / 60(min) / 24(hour)) = 49.7(day)
※いわゆる、49.7日問題

WindowsNT以降では、"GetTickCount64()"というAPIが用意されており、こちらは64bit値(18,446,744,073,709,551,615 ms)なので、ラップアラウンド自体が実用上はあり得ません。
 18,446,744,073,709,551,615 / 1000(ms) / 60(sec) / 60(min) / 24(hour)) = 213503982335(day) = 584,942,417(year)

下記のElapsedTickTime.csはGetTickCount64()をラップし、タイムアウト処理の実装を簡便化できるようにしたラッパークラスです。
ElapsedTickTime.ElapsedTimeMillisecond()でms値を取得して比較する他、ElapsedTickTimeオブジェクトとint値もしくはTimeSpan値を直接比較することもできます。

ダウンロード

【注意事項】引用流用は自由ですが、本内容を使用した際のいかなる結果に関して一切の責任を負いません。自己の責任の上でご活用ください

下記のコード右上にあるコピーボタンをクリックし、任意のエディタに貼り付けて保存してご利用ください。
ライセンスは CC0 とします。

ElapsedTickTime.cs
using System;
using System.Runtime.InteropServices;

namespace MyLib
{
    /// <summary>
    /// GetTickCount64を使用して、時刻補正やラップアラウンドに影響しない経過時間を使用するクラス
    ///
    /// ** License **
    /// - These codes are licensed under CC0.
    /// - http://creativecommons.org/publicdomain/zero/1.0/deed.ja
    /// </summary>
    public class ElapsedTickTime
    {

        // ms単位で返す
        // (精度はシステムタイマーの精度。一般的には10ms~16msの範囲)
        // https://learn.microsoft.com/ja-jp/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64
        [System.Runtime.InteropServices.DllImport("kernel32.DLL")]
        public static extern UInt64 GetTickCount64();

        // 保持しているtick値(ms)
        private UInt64 _tick;
        
        // GetTickCount64チェック済?
        private static bool _isSupportCheck = false;


        /// <summary>
        /// コンストラクタ
        /// 現在のTickで初期化
        /// </summary>
        /// <remarks></remarks>
        public ElapsedTickTime()
        {
            // サポートチェックを一度も行っていない場合、チェックを行う
            if (!_isSupportCheck)
            {
                if (!IsSupport())
                    throw new PlatformNotSupportedException("GetTickCount64 is not support.");

                _isSupportCheck = true;
            }

            // 現在のTickで初期化 
            Reset();
        }


        /// <summary>
        /// 現在のTickで初期化
        /// </summary>
        /// <remarks></remarks>
        public void Reset()
        {
            _tick = GetTickCount64();
        }


        /// <summary>
        /// 経過時間(ms)を文字列で返す
        /// </summary>
        /// <remarks></remarks>
        public override string ToString()
        {
            return ElapsedTimeMillisecond().ToString();
        }


        /// <summary>
        /// 経過時間(ms)を返す
        /// </summary>
        /// <remarks></remarks>
        public UInt64 ElapsedTimeMillisecond()
        {
            UInt64 now = GetTickCount64();

            // NOTE 万一ラップアラウンドが発生したらログに記録
            // 64bit(18,446,744,073,709,551,615 msなので、実用上はあり得ない
            // (18,446,744,073,709,551,615 / 1000(ms) / 60(sec) / 60(min) / 24(hour)) = 213503982335(day) = 584942417(year)!
            if (now < _tick)
                System.Diagnostics.Trace.TraceWarning("ElapsedTickTime.ElapsedTimeMillisecond(), wrapped tick.");

            unchecked
            {
                return now - _tick;
            }
        }


        /// <summary>
        /// GetTickCount64が使えるか確認
        /// </summary>
        /// <remarks></remarks>
        public static bool IsSupport()
        {
            // GetTickCount64はVista/Windows Server 2008以降しか使えないので、その確認
            System.OperatingSystem os = System.Environment.OSVersion;

            // Windows NT系か調べる
            if (os.Platform == PlatformID.Win32NT)
            {
                // メジャーバージョン番号が6以上ならば、Vista(または、Server 2008)以降
                if (os.Version.Major >= 6)
                    return true;
            }

            return false;
        }


        // 
        // 比較演算子(Integer形式のミリ秒で比較)
        // 
        public static bool operator >=(ElapsedTickTime c1, int c2)
        {
            return (c1.ElapsedTimeMillisecond() >= (ulong)c2);
        }


        public static bool operator <=(ElapsedTickTime c1, int c2)
        {
            return (c1.ElapsedTimeMillisecond() <= (ulong)c2);
        }


        public static bool operator >(ElapsedTickTime c1, int c2)
        {
            return (c1.ElapsedTimeMillisecond() > (ulong)c2);
        }


        public static bool operator <(ElapsedTickTime c1, int c2)
        {
            return (c1.ElapsedTimeMillisecond() < (ulong)c2);
        }


        // 
        // 比較演算子(TimeSpan形式のミリ秒で比較)
        // 
        public static bool operator >=(ElapsedTickTime c1, TimeSpan c2)
        {
            return (c1.ElapsedTimeMillisecond() >= c2.TotalMilliseconds);
        }


        public static bool operator <=(ElapsedTickTime c1, TimeSpan c2)
        {
            return (c1.ElapsedTimeMillisecond() <= c2.TotalMilliseconds);
        }


        public static bool operator >(ElapsedTickTime c1, TimeSpan c2)
        {
            return (c1.ElapsedTimeMillisecond() > c2.TotalMilliseconds);
        }


        public static bool operator <(ElapsedTickTime c1, TimeSpan c2)
        {
            return (c1.ElapsedTimeMillisecond() < c2.TotalMilliseconds);
        }
    }
}

サンプル

ElapsedTickTime timeout = new ElapsedTickTime();

while(True) 
{
    // 基本
    if (timeout.ElapsedTimeMillisecond() >= 1000)
    {
        // timeout
        break;
    }

    // intとの比較演算子を実装しているので、下記の書き方もOK
    if (timeout >= 1000)
    {
        // timeout
        break;
    }
 

    // 何らかの処理
    {
    }
}