我遇到了一个罕见的问题,即 SetThreadpoolTimerEx 未按预期调用回调函数。
环境:
操作系统:Windows 10 22H2(内部版本 19045.5131)
Visual Studio:2022(版本 17.12.0)
SDK 和工具集:Windows SDK 10.0.19041.0,平台工具集 v143
问题:
根据 SetThreadpoolTimerEx 的文档(link):
如果定时器之前的状态是“set”,并且函数返回 FALSE,则回调正在进行或即将开始。
但是,在我的例子中,函数返回 FALSE,但回调没有被调用。这导致我的程序在等待回调信号完成时挂起。
要重现的 C++ 代码:
#include <iostream>
#include <atomic>
#include <chrono>
#include <Windows.h>
std::atomic<bool> callbackDone = false;
VOID(NTAPI callback)(
_Inout_ PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID Context,
_Inout_ PTP_TIMER Timer
)
{
printf("callback...\n");
callbackDone = true;
}
int main() {
bool canFail = false;
while (true) {
callbackDone = false;
auto timer = CreateThreadpoolTimer(callback, nullptr, nullptr);
LARGE_INTEGER relativeTime = {};
relativeTime.QuadPart = 10000000 * -1;
FILETIME relativeTimeFt = {};
relativeTimeFt.dwLowDateTime = relativeTime.LowPart;
relativeTimeFt.dwHighDateTime = static_cast<DWORD>(relativeTime.HighPart);
auto setRes1 = SetThreadpoolTimerEx(timer, &relativeTimeFt, 0, 0);
Sleep(999);
auto setRes2 = SetThreadpoolTimerEx(timer, nullptr, 0, 0);
if (setRes2 == TRUE) {
FILETIME now = {};
SetThreadpoolTimerEx(timer, &now, 0, 0);
}
else {
if (!callbackDone) {
WaitForThreadpoolTimerCallbacks(timer, FALSE);
canFail = true;
}
}
if (canFail) {
printf("waiting for possibly failed timer\n");
}
auto startWaitTime = std::chrono::high_resolution_clock::now();
auto waitMsgStep = std::chrono::seconds(10);
auto nextMsgTime = waitMsgStep;
while (!callbackDone) {
SleepEx(10, TRUE);
auto waitDuration = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::high_resolution_clock::now() - startWaitTime
);
if (waitDuration > nextMsgTime) {
printf("waiting for callback to finish for %d seconds...\n", static_cast<DWORD>(nextMsgTime.count()));
nextMsgTime += waitMsgStep;
}
}
if (canFail) {
canFail = false;
printf("timer is ok\n");
}
CloseThreadpoolTimer(timer);
}
}
可能的输出:
问题:
尽管 SetThreadpoolTimerEx 返回 FALSE(根据文档,这表明回调正在进行或即将开始),但在极少数情况下,回调函数不会被调用。这会导致程序无限期地挂起在循环中等待callbackDone 变为true。
问题:
SetThreadpoolTimerEx 是否存在回调的已知问题 即使函数返回 FALSE 也可能不会被调用?
我是否滥用了线程池计时器 API,从而可能导致 这种行为?
如何修改我的代码以确保回调始终 调用或正确处理没有调用的情况?
另外,来自Raymond Chen的博客:
最后一行是有趣的:当您取消计时器或等待时 对象,线程池尝试召回任何挂起的回调,但是 有时回调已经走得太远而无法进行 回忆道。例如,回调可能已经在进行中。在 在这种情况下,Set...Ex 函数返回 FALSE 来告诉您您正在 还没有完成。必须先等待回调完成 一切终于完成了。
在我的例子中,SetThreadpoolTimerEx 返回 FALSE,我等待 10 秒或更长时间,但回调从未调用。
是的,这是 Windows 错误。 23H2 22631.2861 中也存在最小值,我在那里重现它。
最小工作代码可以是:
VOID NTAPI OnTimer(
_Inout_ PTP_CALLBACK_INSTANCE /*Instance*/,
_Inout_opt_ PVOID Context,
_Inout_ PTP_TIMER /*Timer*/
)
{
*(bool*)Context = true;
}
void Test01()
{
bool callbackDone = false;
if (PTP_TIMER Timer = CreateThreadpoolTimer(OnTimer, &callbackDone, 0))
{
LARGE_INTEGER DueTime = { (ULONG)-1000000, -1 }; // 100 ms
SetThreadpoolTimerEx(Timer, (PFILETIME)&DueTime, 0, 0);
Sleep(100);
if (!SetThreadpoolTimerEx(Timer, 0, 0, 0))
{
if (!callbackDone)
{
WaitForThreadpoolTimerCallbacks(Timer, FALSE);
if (!callbackDone)
{
__debugbreak();
}
Sleep(2000);
if (!callbackDone)
{
__debugbreak();
}
}
}
CloseThreadpoolTimer(Timer);
}
}
SetThreadpoolTimerEx
返回 TppCancelTimer
的结果 - 那么计时器是否被取消。如果计时器之前已设置但未取消 - 则必须执行它。调用 WaitForThreadpoolTimerCallbacks
后 - callbackDone
必须已设置。但这里存在实现中的问题:如果我们在计时器已触发时调用 SetThreadpoolTimerEx(Timer, 0, 0, 0)
,但用户回调尚未调用 - 比如说在 TppSingleTimerExpiration
点(这是内部函数,未导出) - SetThreadpoolTimerEx
( TppCancelTimer
)返回 false,但在这种情况下不会调用用户回调。我们可以在 TppSingleTimerExpiration
上设置钩子并延迟它的执行,以重现 Raice:
void (NTAPI *TppSingleTimerExpiration)(PTP_TIMER Timer, PSRWLOCK SRWLock, BOOLEAN b);
HANDLE _G_hEvent = CreateEvent(0, 0, 0, 0);
void Hook_TppSingleTimerExpiration(PTP_TIMER Timer, PSRWLOCK SRWLock, BOOLEAN b)
{
DbgPrint("Hook_TppSingleTimerExpiration(%p) [0]\n", Timer);
SetEvent(_G_hEvent);
Sleep(2000);
DbgPrint("Hook_TppSingleTimerExpiration(%p) [1]\n", Timer);
DetourTransactionBegin();
DetourDetach((void**)&TppSingleTimerExpiration, Hook_TppSingleTimerExpiration);
DetourTransactionCommit();
TppSingleTimerExpiration(Timer, SRWLock, b);
DbgPrint("Hook_TppSingleTimerExpiration(%p) [2]\n", Timer);
}
VOID NTAPI OnTimer(
_Inout_ PTP_CALLBACK_INSTANCE /*Instance*/,
_Inout_opt_ PVOID Context,
_Inout_ PTP_TIMER /*Timer*/
)
{
*(bool*)Context = true;
}
void Test02()
{
if (_G_hEvent = CreateEvent(0, 0, 0, 0))
{
// !! need look for real address on target system
(ULONG_PTR&)TppSingleTimerExpiration = 0x00007FFCB0FA081C;
DetourTransactionBegin();
DetourAttach((void**)&TppSingleTimerExpiration, Hook_TppSingleTimerExpiration);
DetourTransactionCommit();
bool callbackDone = false;
if (PTP_TIMER Timer = CreateThreadpoolTimer(OnTimer, &callbackDone, 0))
{
LARGE_INTEGER DueTime = { (ULONG)-1000000, -1 }; // 100 ms
SetThreadpoolTimerEx(Timer, (PFILETIME)&DueTime, 0, 0);
WaitForSingleObject(_G_hEvent, INFINITE);
if (!SetThreadpoolTimerEx(Timer, 0, 0, 0))
{
if (!callbackDone)
{
WaitForThreadpoolTimerCallbacks(Timer, FALSE);
if (!callbackDone)
{
__debugbreak();
}
Sleep(2000);
if (!callbackDone)
{
__debugbreak();
}
}
}
CloseThreadpoolTimer(Timer);
}
CloseHandle(_G_hEvent);
}
}