【问题标题】:SetTimer() pitfallsSetTimer() 陷阱
【发布时间】:2011-05-05 07:02:54
【问题描述】:

我有一个无窗口计时器(没有 WM_TIMER),它只在给定时间段过去时触发一次回调函数。它被实现为SetTimer()/KillTimer()。时间段足够小:100-300 毫秒。

对于每个如此短的时间间隔调用SetTimer()/KillTimer() pair 是否足够便宜(我的意思是性能)?

如果我有 100 个这样的计时器会定期调用 SetTimer()/KillTimer() 怎么办?系统中可以同时存在多少个Window timer对象?

这是一个问题: 使用一堆这样的定时器对象并依赖于定时器的良好 Windows 实现,或者创建一个每隔 30 毫秒计时的 Windows 定时器对象,并为它订阅所有自定义的 100-300 毫秒的一次性定时器。

谢谢

【问题讨论】:

    标签: c++ windows winapi


    【解决方案1】:

    当您尝试使用计时器消息时,它们的问题在于它们是低优先级消息。实际上,它们是假消息。定时器与底层内核定时器对象相关联——当消息循环检测到内核定时器发出信号时,它只是用一个标志标记当前线程消息队列,指示对 GetMessage 的下一次调用——当没有其他消息要处理时——应该综合WM_TIMER 消息及时返回。

    由于可能有很多计时器对象,系统是否会公平地为所有计时器平等地发出计时器消息并不明显,并且任何系统负载都可以完全阻止 WM_TIMER 消息的生成很长一段时间。

    如果您可以控制消息循环,则可以使用维护自己的计时器事件列表(以及应该发生的 GetTickCount 时间戳)和 MSGWaitForMultipleObject - 而不是 GetMessage 来等待消息。使用 dwTimeout 参数提供最小的时间间隔 - 从现在开始 - 直到下一个计时器发出信号。因此,每次您有一个要处理的计时器时,它都会从等待消息中返回。

    并且/或者您可以使用waitable timers - 在带有 MSGWaitForMultipleObjects 的 GUI 线程上,或仅在工作线程上,直接访问较低级别的计时功能。

    【讨论】:

    • 我不使用 WM_TIMER。我使用无窗口计时器(将时间过程指针直接传递给 ::SetTimer())。问题是集中创建/销毁计时器内核对象的成本是多少,以及有多少这样的对象可以同时存在而没有任何问题。谢谢。
    • 您一直在使用 WM_TIMER。 GetMessage 合成消息,而 DispatchMessage - 当传递带有 NULL hwnd 的 WM_TIMER 消息时,将其直接分派到 TimerProc/LPARAM。
    • 我不确定您所说的“当消息循环检测到火灾时”是什么意思。它的工作方式是系统线程管理用户计时器的完整列表并进行火灾检测并设置标志以唤醒正确的线程。 GetMessage 将看到该标志,如果不存在其他消息,则必须去实际挖掘该计时器。线程唯一知道的是 1) 触发的计时器和 2) 触发的次数。 GetMessage 必须实际调用以扫描整个用户计时器列表并找到与您的线程关联的最早的一个。您的下一个 GetMessage 会找到下一个,依此类推。
    【解决方案2】:

    SetTimer() 最大的缺陷是它实际上是 USER 对象(尽管它没有在 MSDN USER objects list 中列出)因此它属于 Windows USER 对象限制 - 默认情况下每个对象最多 10000 个进程,每个会话最多 65535 个对象(所有正在运行的进程)。

    这可以通过简单的测试轻松证明 - 只需调用 SetTimer()(参数无关紧要,窗口和无窗口的行为方式相同)并在任务管理器中查看 USER 对象数量增加。

    另请参阅 ReactOS ntuser.h 源和 this article。他们都声明TYPE_TIMER 是 USER 句柄类型之一。

    所以要当心 - 创建一堆计时器可能会耗尽您的系统资源并使您的进程崩溃甚至整个系统无响应。

    【讨论】:

      【解决方案3】:

      以下是我觉得你在问这个问题时实际上想要的细节:

      SetTimer()会先扫描非内核定时器列表(双向链表),看定时器ID是否已经存在。如果计时器存在,它将简单地重置。如果不是,则发生 HMAllocObject 调用并为结构创建空间。然后将填充计时器结构并将其链接到列表的头部。

      这将是创建 100 个计时器的总开销。这正是例程所做的,除了检查最小和最大 dwElapsed 参数。

      就计时器到期而言,计时器列表在(大约)最后一次计时器列表扫描期间看到的最小计时器持续时间的持续时间进行扫描。 (实际上,真正发生的是——一个内核定时器被设置为找到的最小用户定时器的持续时间,这个内核定时器唤醒检查用户定时器到期的线程,并通过在他们的消息队列状态。)

      对于列表中的每个计时器,上次扫描计时器列表的时间(以毫秒为单位)与当前时间(以毫秒为单位)之间的当前增量从列表中的每个计时器递减。当一个到期时(剩余

      当您的主消息泵调用 GetMessage() 时,当没有其他消息可用时,GetMessage() 会在线程的唤醒位中检查 QS_TIMER,如果设置 - 通过扫描完整的用户计时器生成 WM_TIMER 消息列表中标记为 READY 且与您的线程 ID 相关联的最小计时器的列表。然后它会减少您的线程 CurrentTimersReady 计数,如果为 0,则清除计时器唤醒位。您对 GetMessage() 的下一次调用将导致相同的事情发生,直到所有计时器都用完为止。

      单次计时器保持实例化。当它们过期时,它们被标记为 WAITING。下一次调用具有相同计时器 ID 的 SetTimer() 将简单地更新并重新激活原始的。单次计时器和周期性计时器都会自行重置,并且只会在 KillTimer 或您的线程或窗口被破坏时终止。

      Windows 实现非常基础,我认为编写一个性能更高的实现对您来说是微不足道的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-12-18
        • 1970-01-01
        • 2011-10-07
        • 2010-10-22
        • 1970-01-01
        相关资源
        最近更新 更多