Windows 上應優先採用事件等待而非 Sleep(1) 的理由

· 更新日期: · · Windows 開發, 同步, 事件, 計時器, 設計

更新紀錄(2 筆,最後更新 2026年09月04日)

本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。

已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.22279417)
補上了日文原文中已有的諮詢引導(consultation_services)。內文沒有改動。 查看更新前的版本 (DOI: 10.5281/zenodo.21616303)
初次發布
引用本文(DOI: 10.5281/zenodo.21616302)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

Go Komura(2026)。〈Windows 上應優先採用事件等待而非 Sleep(1) 的理由〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616302 https://comcomponent.com/zh-TW/blog/2026/03/16/006-windows-timer-vs-event-wait/

DOI(最新版本)
10.5281/zenodo.21616302
DOI(此版本)
10.5281/zenodo.22297141

上一篇Windows 軟即時實務指南談過要避開把週期交給 Sleep 的迴圈。 這一次只把焦點收在一點:為什麼短的 timer wait 不如 event wait 值得優先採用。

這篇文章可以單獨閱讀。 上一篇的結論只有一行:把週期交給 Sleep 的定時查看迴圈,等待時間與醒來時機都沒有保證,所以不要拿它當週期處理的基礎。只要記住這一句,接下來不看上一篇也追得上。

在 Windows 上,只要用 Sleep(1) 或帶短 timeout 的 wait 做出「每隔一段時間看一次」的設計,就一定會受到 system clock 的粒度與 其後的排程延遲的影響。 一般設定下多半以 15.6ms 級的 platform timer resolution 為前提,所以就算心裡想的是「1ms 後再看一次」,實際拿到的等待往往相當粗糙。

反過來說,如果真正想等的不是「時間」而是「事情發生」——工作抵達、I/O 完成、停止請求、狀態變化——就沒有必要固定間隔跑去查看。 由事情發生的一方 signal,等待的一方去等 event,這樣對延遲、對 CPU、對耗電都更直接了當。

事件驅動的等待方式說明真正想等的若不是時間而是事情發生,就不必固定間隔跑去查看,改由事情發生的一方signal、等待的一方等event,對延遲、CPU與耗電都更直接了當的圖。想等的是事情發生發生的一方做signal等待的一方等event對延遲、CPU與耗電都直接了當

圖 1: 要等事情發生,就不要固定間隔查看,讓發生的一方來通知。

這篇文章想回答的問題有下面 4 個。

  • 為什麼 Sleep(1) 或短的 timer wait 沒有想像中準確
  • 為什麼 event wait 比較不受這個限制
  • 什麼場合該選 event 而不是 timer
  • 即使如此,還是該用 timer 的場合是哪些

本文會出現的術語

先把文中不再另行說明的縮寫列在這裡。

術語 意義
platform timer resolution / system clock resolution 作業系統更新時刻的間隔。timed wait 的 timeout 判定會被這個粒度拉走
ISR (Interrupt Service Routine) 中斷發生時以最高優先權執行的處理。它在跑的期間,我們的執行緒只能等
DPC (Deferred Procedure Call) ISR 為了「稍後再做後續」而排入的高優先權延遲處理。在本文中,把它和 ISR 一起讀成 中斷處理帶來的延遲來源就夠了
IOCP (I/O Completion Port) 把非同步 I/O 的完成通知集中到佇列,再由專用執行緒群接收的 Windows 機制
WaitOnAddress 用來「等到某個記憶體位址的值改變為止」的同步 API。只能用在同一處理程序內(5.3 會說明)
signal 把等待方的條件滿足掉。若是 event,就是呼叫 SetEvent

1. 先講結論

  • 要等工作抵達或 I/O 完成時,等 event 會比等 timer 好。
  • Windows 的 timed wait 一定會受到 system clock 粒度的影響。
  • Sleep(1) 的意思不是「1ms 後準時醒來」。
  • 而且 timeout 過了之後,執行緒也只是先變成 ready,並不保證立刻執行。
  • 所以 「其實在等事情發生,卻用 timer 去查看」的設計,對延遲與耗電都不利。
  • 把 timer 的使用收斂到 真的以時間本身為條件的場合,整體會乾淨得多。

換成實務上的說法,大致就是下面幾條。

  • 「每 5 秒送一次 metrics」 -> timer 的工作
  • 「佇列一有工作就立刻動」 -> event / semaphore / condition variable / WaitOnAddress 的工作
  • 「I/O 結束後接著執行後續」 -> completion / event 的工作
  • 「收到停止請求就停下來」 -> stop event / cancellation 的工作
timer與event的界線說明只有在真正以時間本身為條件時才使用timer,等待工作抵達、I/O完成、停止請求這類事情發生時就去等event的界線的圖。時間本身事情發生想等的是時間還是事情發生timer的工作等event的工作工作抵達 / I/O完成 / 停止請求

圖 2: timer 只用在以時間本身為條件的場合,事情發生就用 event 等。

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 21 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

2. 問題出在哪裡

2.1 timed wait 受制於 system clock 的粒度

Windows 的 wait functions,其 timeout 精度取決於 system clock resolution。 Sleep 也一樣,指定的毫秒數並不保證就是「剛好那麼長」。

這裡的重點是:指定 1ms,不代表 1ms 後就會醒來。

查看自己環境的粒度

「15.6ms 級」只是通則,手邊機器的實際值自己查一次比較快。查法有 2 種。

第一種是呼叫 GetSystemTimeAdjustment。第 2 個引數 lpTimeIncrement 會傳回 系統更新 time-of-day clock 的間隔,單位是 100 奈秒。如果是 15.6ms 級,值會落在 15 萬多。

#include <windows.h>
#include <cstdio>

int main()
{
    DWORD adjustment = 0;
    DWORD increment = 0;
    BOOL adjustmentDisabled = FALSE;

    if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled))
    {
        std::printf("GetSystemTimeAdjustment failed. GetLastError=%lu\n", GetLastError());
        return 1;
    }

    // increment 的單位是 100ns,換算成 ms 來看
    std::printf("time increment = %lu (100ns) = %.4f ms\n",
                increment,
                increment / 10000.0);
    return 0;
}

第二種是執行 Sysinternals 的 ClockRes。它內部呼叫的就是同一個 GetSystemTimeAdjustment,只是把 system clock 的解析度,也就是應用程式能拿到的最大 timer resolution 顯示出來的小工具。不想寫程式碼就想查看時,用這個比較快。

另外,文件上把 lpTimeIncrement 說明成 系統在開機時決定的固定值,運轉中不會改變。也就是說,這個值是用來知道「這台機器原本的粒度」,不是用來量測呼叫 timeBeginPeriod 之後的結果。那部分在 6.3 會提到。

查看原本粒度的兩種方法說明自己環境的timer粒度可以呼叫GetSystemTimeAdjustment查看以100奈秒為單位的更新間隔,或執行內部呼叫同一個API的Sysinternals ClockRes來確認的圖。想知道手邊機器的粒度呼叫GetSystemTimeAdjustment執行ClockRes傳回以100奈秒為單位的更新間隔內部呼叫的是同一個API

圖 3: 用程式碼呼叫或用 ClockRes,兩種方式都能查看環境原本的粒度。

2.2 時間到了,也不一定馬上執行

更麻煩的是,timeout 過去的那一瞬間,執行緒並不會立刻被執行。

如同 Sleep 的說明所寫,等待時間結束後執行緒會變成 ready,但 不保證馬上就能拿到 CPU 執行。 還會受到其他執行緒、優先權、CPU 的 idle state、DPC / ISR、鎖競爭等影響。

也就是說,短的 timer wait 至少有 2 層不確定性。

  1. timeout 的判定本身就會被 timer 粒度拉走
  2. timeout 之後,何時開始執行仍然要看 scheduler
短timer wait的兩層不確定性說明短的timer wait中timeout的判定本身會被timer粒度拉走,而且timeout之後執行緒也只是變成ready、開始執行仍要看scheduler,總共有兩層不確定性的圖。開始短的timer waittimeout判定取決於timer粒度執行緒只是變成ready開始執行要看scheduler受其他執行緒與DPC / ISR影響

圖 4: timeout 判定與開始執行這兩處,都會偏離指定的等待時間。

2.3 Sleep(1) 不等於 1ms 週期

看到 Sleep(1),很容易覺得這是「每 1ms 跑一圈的 loop」。 但實際上不該這樣讀。

while (!g_stop)
{
    Step();
    Sleep(1);
}

這個 loop 的真實情況是這樣。

  • 每一圈都要加上 Step() 的執行時間
  • Sleep(1) 的等待時間本身會被粒度拉走
  • 就算醒來,也不一定能馬上跑
Sleep(1)迴圈的真實情況說明Sleep(1)的迴圈每一圈都會加上Step的執行時間,等待本身被粒度拉走,醒來後也不一定能馬上跑,所以不會構成1毫秒週期的圖。加上Step的執行時間不會變成1毫秒週期等待被粒度拉走醒來後不一定能馬上跑

圖 5: 三種偏差疊在一起,所以 Sleep(1) 不會是每 1 毫秒一次的週期。

3. 為什麼事件等待比較有利

3.1 等待的結束條件從「時間到」變成「signal」

event wait 之所以有利,是因為等待的意義本身變了。

timer wait 是這樣。

  • 就算什麼都還沒發生
  • 時間一到就醒來
  • 醒來之後才去檢查「有沒有發生什麼」

event wait 是這樣。

  • 事情發生的一方去 signal
  • 被 signal 之後,等待才算滿足
  • 醒來的當下,就已經有理由

畫成圖就看得出來,等待結束的方式本身就不一樣。

event wait: 因為發生了才被叫醒發生的一方做 signal等待醒來的當下理由已經確定進行處理timer wait: 因為時間到了才醒沒有有以timer粒度醒來等待有發生什麼嗎?進行處理

圖 6: 只有 timer wait 有空轉的迴圈,event wait 在醒來時理由已經確定。

只有 timer wait 這一側,才有 空轉一趟再回去的迴圈。這一點在延遲與耗電兩方面都會顯現出來。

3.2 依照要等什麼來分工具

那麼實際上要選哪個工具?第一層判斷,大致用這張表就夠了。

想等的東西 不好的寫法 第一選擇
佇列裡出現工作 用 Sleep(1) 去 TryPop event / semaphore
I/O 完成 用 timer 去查看狀態 overlapped I/O 的 event / IOCP
收到停止請求 每 100ms 看一次 stop flag stop event / cancellation
同一處理程序內的值變化 while (flag == 0) Sleep(1) WaitOnAddress
時刻到來 硬湊成用 event 等 timer / waitable timer

3.3 event 也不是魔法

event wait 的優勢在於 不必依 timer 粒度醒來,但這不代表 被 signal 的瞬間就一定零延遲開跑。

即使是 event wait,也一樣會受到下面這些影響。

  • scheduler latency
  • thread priority
  • CPU 的 power state
  • 鎖競爭
  • page fault
  • DPC / ISR

不過至少 「睡到下一個 timer tick 為止」這種多餘的等法可以拿掉。

event wait能拿掉的等待與仍在的影響說明即使用event wait也仍會受scheduler latency、thread priority、DPC / ISR等影響,但可以拿掉睡到下一個timer tick為止這種多餘等法的圖。用event wait等待scheduler等影響仍在可拿掉睡到timer tick的等待signal後並非零延遲

圖 7: event 不是魔法,但至少一定不必再依 timer 粒度醒來。

4. 典型的反模式

4.1 用 Sleep(1) 輪詢佇列

最常見到的就是這個。

for (;;)
{
    if (g_stop)
    {
        break;
    }

    WorkItem item;
    if (TryPop(item))
    {
        Process(item);
        continue;
    }

    Sleep(1);
}

這種寫法乍看單純,但有 3 個問題。

  1. 佇列是空的也會定期醒來
  2. latency 被 timer 粒度拉走
  3. 耗電上也吃虧
Sleep(1)輪詢的三個問題說明用Sleep(1)輪詢佇列會有佇列是空的也定期醒來、延遲被timer粒度拉走、耗電上也吃虧這三個問題的圖。用Sleep(1)查看佇列空的也會定期醒來latency被粒度拉走power上也吃虧

圖 8: 乍看單純的輪詢迴圈,在醒來、延遲、耗電三方面都吃虧。

4.2 用 Thread.Sleep(1) / Task.Delay(1) 監控狀態

C# / .NET 也會出現同樣的味道。

while (!stoppingToken.IsCancellationRequested)
{
    if (_queue.TryDequeue(out WorkItem? item))
    {
        await ProcessAsync(item, stoppingToken);
        continue;
    }

    await Task.Delay(1, stoppingToken);
}

外表是 async、看起來很溫和,但設計的本質仍然是 polling。

5. 這樣修正

5.1 由 producer 在抵達時 signal

如果等的是 queue 的抵達,就把 polling 改成 由 producer 做 signal 的形式。

  • producer 把 item 放進 queue
  • 放入 item 之後立刻呼叫 SetEvent
  • consumer 用 WaitForSingleObject 或 WaitForMultipleObjects 等待
  • 醒來之後把 queue drain 掉
producer做signal的形式說明producer把item放進佇列後立刻SetEvent,consumer用WaitForSingleObject等函式等待,醒來後把佇列drain掉的流程的圖。consumerqueueproducerconsumerqueueproducer放入item之後立刻SetEvent從wait醒來把queue drain掉

圖 9: 由知道抵達的 producer 那一側通知,consumer 就不會再空轉。

5.2 用 WaitForMultipleObjects 同時等 work 與 stop

如果是單純的 worker,這個形式最好懂。

HANDLE waits[2] = { _stopEvent, _workEvent };  // index 0 = stop, index 1 = work

for (;;)
{
    // bWaitAll = FALSE,所以傳回值是「最先被 signal 的 handle 的 index」
    DWORD rc = WaitForMultipleObjects(2, waits, FALSE, INFINITE);

    // 失敗是 WAIT_FAILED ((DWORD)0xFFFFFFFF)。原因只能靠 GetLastError 得知
    if (rc == WAIT_FAILED)
    {
        throw std::system_error(
            static_cast<int>(GetLastError()),
            std::system_category(),
            "WaitForMultipleObjects failed.");
    }

    if (rc == WAIT_OBJECT_0)  // stop
    {
        return;
    }

    if (rc == WAIT_OBJECT_0 + 1)  // work
    {
        DrainQueue();
        continue;
    }

    // 在 INFINITE 等待下走到這裡屬於非預期
    // (WAIT_TIMEOUT 或 WAIT_ABANDONED_0 這一類)。不要吞掉,直接讓它掛掉
    throw std::runtime_error("WaitForMultipleObjects returned an unexpected value.");
}

這個例子有 3 個重點。

  • Sleep(1) 消失了
  • item 抵達時由 producer 呼叫 SetEvent
  • worker 同時等 stop 與 work

只有傳回值的處理,實務上特別容易做錯,這裡補充幾點。

  • bWaitAll 為 FALSE 時,成功的傳回值落在 WAIT_OBJECT_0 到 WAIT_OBJECT_0 + nCount - 1 這個範圍,減掉 WAIT_OBJECT_0 之後就是 陣列的 index。只寫 == WAIT_OBJECT_0 與 != WAIT_OBJECT_0 + 1 這兩個分支,handle 一增加到 3 個就會壞掉
  • 多個同時被 signal 時,傳回的是 index 較小的那一個。上面的例子把 stop 放在 index 0,就是為了不漏掉停止請求
  • 失敗不是以例外,而是以 WAIT_FAILED((DWORD)0xFFFFFFFF) 這個傳回值回來。原因不呼叫 GetLastError 就不會知道。如果把 rc != 預期值 一律當成「失敗」寫掉,handle 已經被關閉、沒有 SYNCHRONIZE 權限這類原因就消失了
  • 如果等待對象裡混了 mutex,WAIT_ABANDONED_0 這一類也可能傳回。這個例子只等 event,所以當成非預期來處理

5.3 同一處理程序內,WaitOnAddress 也是候選

在同一個處理程序內,如果只是想「等到某個值改變為止」,WaitOnAddress 也相當有力。 不必再建立 event、初始化它,還要照顧值與同步不要對不上。

取捨的感覺大致如下。

觀點 event / semaphore / waitable object WaitOnAddress
等待對象的範圍 跨處理程序也可以。可以具名 只限同一處理程序內
叫醒的一方 SetEvent / ReleaseSemaphore 等 WakeByAddressSingle / WakeByAddressAll
事前準備 需要建立核心物件並管理 handle 有要等的變數就夠
可用的版本 很早以前就能用 Windows 8 / Windows Server 2012 以後
連結的程式庫 Kernel32.lib Synchronization.lib

使用時有 3 個不想漏掉的重點。

  1. 一定要和 WakeByAddressSingle 或 WakeByAddressAll 成對使用。 改寫值的一方不呼叫它,等待中的執行緒就不會醒。只叫醒 1 條就用 Single,全部叫醒就用 All
  2. WaitOnAddress 即使沒有被 signal 也可能傳回。 文件裡也明寫了在記憶體不足等狀況下可能提早醒來。傳回之後一定要再讀一次值,用 while 迴圈確認它是不是真的變了
  3. 可以等待的大小是 1 / 2 / 4 / 8 位元組其中之一
  4. 旗標要用 atomic(不可分割)的變數。WakeByAddressSingle 只是叫醒等待中的執行緒,並不會讓前一個寫入變成不可分割,也不會讓它變成可見。用原始變數從兩邊讀寫,在 C++ 是資料競爭(未定義行為),在最佳化過的建置中可能看不到更新而一直停住。寫的一方用 release,讀的一方用 acquire,兩邊對齊
// 等待方與叫醒方是不同執行緒,所以旗標一定要用 atomic。
// 用原始的 ULONG 從兩邊讀寫,在 C++ 是資料競爭(未定義行為),
// 在最佳化過的建置中,值可能一直留在暫存器裡而看不到更新,
// 就算被叫醒也可能一直停住
std::atomic<ULONG> g_ready{ 0 };
static_assert(std::atomic<ULONG>::is_always_lock_free,
              "要傳給 WaitOnAddress,所以必須是無鎖的");

// 「等到 g_ready 不再是 0 為止」的最小寫法
ULONG undesired = 0;
ULONG captured = g_ready.load(std::memory_order_acquire);

while (captured == undesired)
{
    // 可能提早傳回,所以傳回之後一定要重讀一次
    WaitOnAddress(&g_ready, &undesired, sizeof(ULONG), INFINITE);
    captured = g_ready.load(std::memory_order_acquire);
}

改寫的一方要先更新值,再把對方叫醒。

// 用 release 寫入。這樣寫的話,這一行之前準備好的資料(下面的 payload),
// 從用 acquire 讀取的一方一定看得到。保證順序的是這個 store,
// 而不是 WakeByAddressSingle ── 它只會叫醒等待中的執行緒,
// 並不會讓前一個寫入變成不可分割,也不會讓它變成可見
g_payload = ...;                                  // 想一起傳過去的資料
g_ready.store(1, std::memory_order_release);
WakeByAddressSingle(&g_ready);
WaitOnAddress的成對流程說明改寫的一方先準備資料、用release寫入旗標後再以WakeByAddressSingle叫醒,等待的一方為了因應提早傳回而用acquire重讀值的while迴圈等待,這組成對流程的圖。等待的一方atomic旗標改寫的一方等待的一方atomic旗標改寫的一方用acquire讀取等到改變為止WaitOnAddress用release寫入WakeByAddressSingle醒來後重讀

圖 10: 負責叫醒的是 WakeByAddress,保證值可見的則是 release 與 acquire 這一側。

6. 仍然要用 timer 的場合

6.1 條件就是時間本身的時候

當然,timer 也有它該登場的場合。

  • 每 5 秒送一次 metrics
  • 200ms 後 retry
  • 每 1 分鐘清一次快取
  • 等到期限時刻就判定 timeout

這些場合想等的 真的就是時間。

6.2 使用 waitable timer

在 Windows 上要等「時間本身」,比起隨手疊 Sleep,使用 waitable timer 的語意清楚得多。

6.3 不要把 timeBeginPeriod 當日常手段

一旦在意短 timer wait 的精度,就很容易想加上一句 timeBeginPeriod(1)。 但不要把它當成日常的第一選擇。

理由有 3 個。

  1. 有 power / performance 的代價
  2. 最近的 Windows 上行為稍微複雜一些
  3. 多半沒有修到真正的原因
不把timeBeginPeriod當日常手段的理由說明就算在意短timer wait的精度,也不要把timeBeginPeriod當成日常第一選擇的理由,是它有power與performance的代價、最近的Windows上行為較複雜,而且多半沒有修到真正的原因的圖。在意精度想加上timeBeginPeriod不要當成日常的第一選擇有代價行為稍微複雜沒有修到真正的原因

圖 11: 在提高精度之前,先想起不該把它當日常手段的 3 個理由。

7. 審查時的檢查清單

  • 有沒有用 Sleep(1) / Thread.Sleep(1) / Task.Delay(1) 寫出定時查看的 loop
  • 明明等的是 queue 抵達、I/O 完成、停止請求,卻用 timer 輪詢
  • 設計上是不是能由 producer / completion 那一側做 signal
  • stop 與 work 能不能用一次 wait 一起等
  • 同一處理程序內的值變化,能不能改用 WaitOnAddress 寫
  • 用到 timer 的地方,真正想等的是不是「時間」

8. 總結

在 Windows 上用短的 timer wait 做出「每隔一段時間看一次」的設計,一定會受到 timer 粒度與 scheduler 的影響。 因此 Sleep(1) 或短 timeout 的等待,並沒有看起來那麼準確。

反過來說,如果真正想等的是工作抵達、I/O 完成、停止請求、狀態變化這類「事情發生」,用 event wait 就自然得多。

歸納起來,就是這一行。

等時間就用 timer,等事情發生就用 event。

只要這條界線清楚,

  • latency 變得好推估
  • 多餘的 periodic wakeup 變少
  • 耗電上也會好一些
  • 程式碼的意圖更容易看懂

這些好處就會逐一顯現出來。

時間用timer、事情發生用event說明只要把等時間就用timer、等事情發生就用event這條界線劃清楚,latency就會變得好推估、多餘的periodic wakeup會減少、耗電上也會好一些、程式碼的意圖也更容易看懂的圖。時間事情發生想等的是哪一種用timer等用event等界線變得清楚latency變得好推估多餘的wakeup變少

圖 12: 只要守住這一行界線,延遲、耗電與程式碼意圖的可讀性都會不一樣。

9. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

為什麼 Windows 的 Sleep(1) 不會在 1 毫秒後準確醒來?
因為 Windows 的 timed wait,其 timeout 精度取決於 system clock resolution,而一般設定下往往以 15.6 毫秒級的 platform timer resolution 為前提。而且等待時間結束後,執行緒也只是變成 ready,並不保證能立刻拿到 CPU 執行,還會受其他執行緒、優先權、CPU 的 idle state、DPC/ISR、鎖競爭等影響。也就是說,短的 timer wait 至少有兩層不確定性:timeout 的判定會被 timer 粒度拉走,以及 timeout 之後何時開始執行要看 scheduler。
用 Sleep(1) 或 Task.Delay(1) 輪詢佇列的設計有什麼問題?
有三個問題:佇列是空的時候也會定期醒來、延遲會被 timer 粒度拉走、耗電上也吃虧。C# 的 await Task.Delay(1) 迴圈看起來溫和,設計上的本質仍然是 polling。修正方式是改成 producer 把 item 放進佇列後立刻呼叫 SetEvent,consumer 用 WaitForSingleObject 或 WaitForMultipleObjects 等待。把 stop 事件與 work 事件放在一次 wait 裡一起等,就能立刻回應停止請求。
計時器等待與事件等待該怎麼分開使用?
界線是:等時間就用 timer,等事情發生就用 event。像每 5 秒送一次 metrics 這種以時間本身為條件的處理,是 waitable timer 的工作。佇列的工作抵達適合用 event 或 semaphore,I/O 完成適合用 overlapped I/O 的 event 或 IOCP,停止請求適合用 stop event 或 cancellation,同一處理程序內的值變化適合用 WaitOnAddress。這條界線清楚之後,延遲會變得好推估,多餘的 periodic wakeup 會減少,程式碼的意圖也更容易看懂。
用 timeBeginPeriod(1) 提高計時器精度不就解決了嗎?
不建議把它當成日常的第一選擇。理由有三個:power 與 performance 都有代價、最近的 Windows 上行為變得比較複雜,以及多半沒有修到真正的原因。如果真正想等的是工作抵達或 I/O 完成這類事情發生,與其提高計時器精度,不如把設計改成由發生的一方 signal 的事件驅動,這對延遲、CPU 和耗電都更直接了當。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽