.NET中如何實現高精度定時器

来源:https://www.cnblogs.com/czwy/archive/2023/12/20/17915333.html
-Advertisement-
Play Games

前言 國產資料庫作為國產化替代的重要環節,在我國信創產業政策的指引下實現加速發展,我們國產資料庫已進入百花齊放的快速發展期,相信接觸到政府類等項目的童鞋尤為瞭解,與此同時我們有一部分也在使用各種開源的ORM都早已支持主流國產資料庫,我們也有一部分在使用官方EF Core但沒有對國產資料庫的統一的管理 ...


.NET中有多少種定時器一文介紹過.NET中至少有6種定時器,但精度都不是特別高,一般在15ms~55ms之間。在一些特殊場景,可能需要高精度的定時器,這就需要我們自己實現了。本文將討論高精度定時器實現的思路。

高精度定時器

一個定時器至少需要考慮三部分功能:計時、等待、觸發模式。計時是進行時間檢查,調整等待的時間;等待則是用來跳過指定的時間間隔。觸發模式是指定時器每次Tick的時間固定還是每次定時任務時間間隔固定。比如定時器時間間隔10ms,定時任務耗時7ms,是每隔10ms觸發一次定時任務,還是等定時任務執行完後等10ms再觸發下一個定時任務。

計時

Windows提供了可用於獲取高精度時間戳或者測量時間間隔的API。系統原生API是QueryPerformanceCounter (QPC)。在.NET中提供了System.Diagnostics.Stopwatch類獲取高精度時間戳,它內部也是通過QueryPerformanceCounter (QPC)進行高精度計時。
QueryPerformanceCounter (QPC)使用硬體計數器作為其基礎。硬體計時器由三個部分組成:時鐘周期生成器、計數時鐘周期的計數器和檢索計數器值的方法。這三個分量的特征決定了QueryPerformanceCounter (QPC)的解析度、精度、準確性和穩定性[1]。它的精度可以高達幾十納秒,用來實現高精度定時器基本沒什麼問題。

等待

等待策略通常有兩種:

  • 自旋:讓CPU空轉等待,一直占用CPU時間。
  • 阻塞:讓線程進入阻塞狀態,出讓CPU時間片,滿足等待時間後切換回運行狀態。

自旋等待

自旋等待可以使用Thread.SpinWait(int iteration)來實現,參數iteration是迭代次數。由於CPU速度可能是動態的,所以很難根據iteration計算消耗的時間,最好是結合Stopwatch使用:

void Spin(Stopwatch w, int duration)
{
    var current = w.ElapsedMilliseconds;
    while ((w.ElapsedMilliseconds - current) < duration)
        Thread.SpinWait(5);
}

由於自旋是以消耗CPU為代價的,上述代碼運行時,CPU處於滿負荷工作狀態(使用率持續保持100%左右),因此短暫的等待可以考慮自旋,長時間運行的定時器不太建議使用該方法。

阻塞等待

阻塞等待需要操作系統能夠及時把定時器線程調度回運行狀態。預設情況下,Windows的系統的計時器精度為15ms左右。如果是線程阻塞,出讓其時間片進行等待,然後再被調度運行的時間至少是一個時間切片15ms左右。要通過阻塞實現高精度計時,則需要減少時間切片的長度。Windows系統API提供了timeEndPeriod可以把計時器精度修改到1ms,在使用計時器服務之前立即調用timeEndPeriod,併在使用完計時器服務後立即調用timeEndPeriodtimeEndPeriodtimeEndPeriod必須成對出現。

在Windows 10, version 2004之前,timeEndPeriod會影響全局Windows設置,所有進程都會使用修改後的計時精度。從Windows 10, version 2004開始,只有調用timeEndPeriod的進程受到影響。
設置更高的精度可以提高等待函數中超時間隔的準確性。 但是,它也可能會降低整體系統性能,因為線程計劃程式更頻繁地切換任務。 高精度還可以阻止 CPU 電源管理系統進入節能模式。 設置更高的解析度不會提高高解析度性能計數器的準確性。[2]

通常我們使用Thread.Sleep來掛起線程等待,Sleep的參數最小為1ms,但實際上很不穩定,實測發現大部分時候穩定在阻塞2ms。我們可以採用Sleep(0)或者Thread.Yield結合Stopwatch計時的方式修正。

void wait(Stopwatch w, int duration)
{
    var current = w.ElapsedMilliseconds;
    while ((w.ElapsedMilliseconds - current) < duration)
        Thread.Sleep(0);
}

Thread.Sleep(0)和Thread.Yield在 CPU 高負載情況下非常不穩定,可能會產生更多的誤差。因此誤差修正最好通過自旋方式實現。

還有一種阻塞的方式是多媒體定時器timeSetEvent,也是網上關於高精度定時器提得比較多的一種方式。它是winmm.dll中的函數,穩定性和精度都比較高,能提供1ms的精度。
官方文檔中說timeSetEvent是一個過時的方法,建議使用CreateTimerQueueTimer替代[3]。但CreateTimerQueueTimer的精度和穩定性都不如多媒體定時器,所以在需要高精度定時器時,還是要用timeSetEvent。以下是封裝多媒體定時器的例子

public enum TimerError
{
    MMSYSERR_NOERROR = 0,
    MMSYSERR_ERROR = 1,
    MMSYSERR_INVALPARAM = 11,
    MMSYSERR_NOCANDO = 97,
}

public enum RepeateType
{
    TIME_ONESHOT=0x0000,
    TIME_PERIODIC = 0x0001
}

public enum CallbackType
{
    TIME_CALLBACK_FUNCTION = 0x0000,
    TIME_CALLBACK_EVENT_SET = 0x0010,
    TIME_CALLBACK_EVENT_PULSE = 0x0020,
    TIME_KILL_SYNCHRONOUS = 0x0100
}

public class HighPrecisionTimer
{
    private delegate void TimerCallback(int id, int msg, int user, int param1, int param2);

    [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")]
    private static extern TimerError TimeGetDevCaps(ref TimerCaps ptc, int cbtc);

    [DllImport("winmm.dll", EntryPoint = "timeSetEvent")]
    private static extern int TimeSetEvent(int delay, int resolution, TimerCallback callback, int user, int eventType);

    [DllImport("winmm.dll", EntryPoint = "timeKillEvent")]
    private static extern TimerError TimeKillEvent(int id);

    private static TimerCaps _caps;
    private int _interval;
    private int _resolution;
    private TimerCallback _callback;
    private int _id;

    static HighPrecisionTimer()
    {
        TimeGetDevCaps(ref _caps, Marshal.SizeOf(_caps));
    }

    public HighPrecisionTimer()
    {
        Running = false;
        _interval = _caps.periodMin;
        _resolution = _caps.periodMin;
        _callback = new TimerCallback(TimerEventCallback);
    }

    ~HighPrecisionTimer()
    {
        TimeKillEvent(_id);
    }

    public int Interval
    {
        get { return _interval; }
        set
        {
            if (value < _caps.periodMin || value > _caps.periodMax)
                throw new Exception("invalid Interval");
            _interval = value;
        }
    }

    public bool Running { get; private set; }

    public event Action Ticked;

    public void Start()
    {
        if (!Running)
        {
            _id = TimeSetEvent(_interval, _resolution, _callback, 0,
                (int)RepeateType.TIME_PERIODIC | (int)CallbackType.TIME_KILL_SYNCHRONOUS);
            if (_id == 0) throw new Exception("failed to start Timer");
            Running = true;
        }
    }

    public void Stop()
    {
        if (Running)
        {
            TimeKillEvent(_id);
            Running = false;
        }
    }

    private void TimerEventCallback(int id, int msg, int user, int param1, int param2)
    {
        Ticked?.Invoke();
    }
}

觸發模式

由於定時任務執行時間不確定,並且可能耗時超過定時時間間隔,定時器的觸發可能會有三種模式:固定時間框架,可推遲時間框架,固定等待時間。

  • 固定時間框架:儘量按照設定的時間來執行任務,只要任務不是始終超時,就可以回到原來的時間框架上
  • 可推遲時間框架:也是儘量按照設定的時間執行任務,但是超時的任務會推遲時間框架。
  • 固定等待時間:不管任務執行時長,每次任務執行結束到下一次任務開始執行間的等待時間固定。

假定時間間隔為10ms,任務執行的時間在7~11ms之間,下圖中顯示了三種觸發模式的區別。
image

其實還有一種觸發模式:任務執行時長大於時間間隔時,只要時間間隔一到,就執行定時任務,多個定時任務併發執行。之所以這裡沒有提及這種模式,是因為在高精度定時場景中,執行任務的時間開銷很有可能大於定時器的時間間隔,如果開啟新線程執行定時任務,可能會占用大量線程,這個需要結合實際情況考慮如何執行定時任務。這裡討論的是預設在定時器線程上執行定時任務。


  1. https://learn.microsoft.com/en-us/windows/win32/sysinfo/acquiring-high-resolution-time-stamps#low-level-hardware-clock-characteristics ↩︎

  2. https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod?redirectedfrom=MSDN ↩︎

  3. https://learn.microsoft.com/en-us/previous-versions//dd757634(v=vs.85)?redirectedfrom=MSDN ↩︎


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`TabWidget`標簽組件的常用方法及靈活運用。`QTabWidget` 是Qt中用於實現標簽頁(tabbed... ...
  • JavaSE學習思維導圖 目錄1 Java語言概述1.1 Java 概述1.2 Java 語言簡史1.3 Java 之父1.4 Java 技術體系平臺2 Java 開發環境搭建2.1 JDK JRE2.2 JDK版本的選擇2.3 JDK 的下載2.4 JDK 的安裝2.5 配置path環境變數2.5 ...
  • 本文主要介紹了設計模式中的狀態模式,併在此基礎上介紹了Spring狀態機相關的概念,並根據常見的訂單流轉場景,介紹了Spring狀態機的使用方式。文中如有不當之處,歡迎在評論區批評指正。 ...
  • 優化內容 這篇不聊技術點,說一下優化後的Python機器人代碼怎麼使用,優化內容如下: 將hook庫獨立成一個庫,發佈到pypi,可使用pip安裝 將微信相關的代碼發佈成另一個庫,也可以pip安裝 git倉庫統一,以後都在這個倉庫更新,不再一篇文章一個倉庫 開始建群,根據群里反饋增加功能和修複bug ...
  • 數據的預處理是數據分析,或者機器學習訓練前的重要步驟。通過數據預處理,可以 提高數據質量,處理數據的缺失值、異常值和重覆值等問題,增加數據的準確性和可靠性 整合不同數據,數據的來源和結構可能多種多樣,分析和訓練前要整合成一個數據集 提高數據性能,對數據的值進行變換,規約等(比如無量綱化),讓演算法更加 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`QStyledItemDelegate`自定義代理組件的常用方法及靈活運用。在Qt中,`QStyledItemD... ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他們的程式出現了CPU爆高,讓我幫忙看下怎麼回事?這種問題好的辦法就是抓個dump丟給我,推薦的工具就是用 procdump 自動化抓捕。 二:Windbg 分析 1. CPU 真的爆高嗎 還是老規矩,要想找到這個答案,可以使用 !tp 命令。 0: ...
  • Lock、Monitor線程鎖 官網使用 https://learn.microsoft.com/zh-cn/dotnet/api/system.threading.monitor?view=net-8.0 一. Lock 1.1介紹 Lock關鍵字實際上是一個語法糖,它將Monitor對象進行封裝 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...