如何调用线程队列中所有APC?代码测试异常求排查
微软官方文档说明:当用户模式APC被排入队列后,线程只有处于可警报状态时才会调用APC函数。线程进入可警报状态后,会按先进先出(FIFO)顺序处理所有待处理的APC,等待操作返回WAIT_IO_COMPLETION。
我理解单次调用SleepEx(0,TRUE)应该能触发线程处理所有待处理的APC,于是写了个程序验证,但结果不符合预期。程序创建WaitableTimer对象,试图多次将自定义的Triggered()函数排入APC队列。主线程先调用Sleep()休眠几秒,让定时器累积多个APC,之后调用SleepEx(0,TRUE)清理队列(预期处理速度比定时器新增APC快)。预期Triggered()会被多次调用,但实际只执行了一次。求排查概念或代码错误。
代码
#include <stdio.h> #include <Windows.h> #include <cassert> void Triggered([[maybe_unused]] LPVOID lpArgToCompletionRoutine,[[maybe_unused]] DWORD dwTimerLowValue,[[maybe_unused]] DWORD dwTimerHighValue) { printf("TRIGGERED!\n"); } int wmain() { HANDLE hTimer{ ::CreateWaitableTimerW(nullptr, TRUE, L"MyTimer") }; assert(hTimer); SYSTEMTIME st{}; FILETIME ft{}; st.wYear = 2020; st.wDay = 15; st.wHour = 15; st.wMonth = 10; st.wMinute = 15; ::TzSpecificLocalTimeToSystemTime(nullptr, &st, &st); ::SystemTimeToFileTime(&st, &ft); LARGE_INTEGER dueTime{}; dueTime.QuadPart = *(LONGLONG*)&ft;//<<<<我也试过设置相对的lpDueTime值 assert(::SetWaitableTimer(hTimer, &dueTime, 100, Triggered, nullptr, FALSE)); ::Sleep(1000); ::SleepEx(0,TRUE); /* 我也试过用下面的代码替换::SleepEx(0,TRUE) ** DWORD ret{WAIT_IO_COMPLETION}; ** while (ret == WAIT_IO_COMPLETION) ** { ** ret=::SleepEx(0, TRUE); ** } */ assert(::CancelWaitableTimer(hTimer)); return 0; }
问题分析与解决
核心错误点
WaitableTimer的APC队列逻辑误解:当使用
SetWaitableTimer设置周期性定时器时,每次定时器到期只会向队列排入一个APC,且存在关键限制:如果前一次的APC还未被处理,系统不会为后续的定时器到期事件重复排入APC。也就是说,队列中最多只会存在一个该定时器的APC,不会累积多个。这就是你只看到一次TRIGGERED!输出的直接原因。SleepEx(0, TRUE)的行为:
SleepEx(0, TRUE)确实会让线程进入可警报状态并立即检查APC队列,处理所有待处理的APC后返回,但由于定时器的APC根本没有累积,所以自然只会执行一次。
修正方案
方案1:手动排入APC验证逻辑
如果要验证“处理所有待处理APC”的逻辑,不能依赖WaitableTimer的周期性APC,改用QueueUserAPC手动向线程队列排入多个APC即可:
#include <stdio.h> #include <Windows.h> #include <cassert> void Triggered([[maybe_unused]] ULONG_PTR dwParam) { printf("TRIGGERED!\n"); } int wmain() { // 手动排入3个APC for(int i=0; i<3; ++i) { QueueUserAPC((PAPCFUNC)Triggered, GetCurrentThread(), 0); } // 处理所有APC DWORD ret = WAIT_IO_COMPLETION; while(ret == WAIT_IO_COMPLETION) { ret = SleepEx(0, TRUE); } return 0; }
这段代码会输出3次TRIGGERED!,符合“单次进入可警报状态处理所有待处理APC”的预期。
方案2:用WaitableTimer实现多次触发
如果一定要用WaitableTimer实现多次APC触发,需要确保每次APC处理完成后,定时器的下一次到期才会排入新的APC。让线程持续处于可警报状态,及时处理每个APC即可:
#include <stdio.h> #include <Windows.h> #include <cassert> HANDLE g_hTimer; void Triggered([[maybe_unused]] LPVOID lpArgToCompletionRoutine,[[maybe_unused]] DWORD dwTimerLowValue,[[maybe_unused]] DWORD dwTimerHighValue) { static int count = 0; printf("TRIGGERED! %d\n", ++count); // 触发10次后停止定时器 if(count >= 10) { CancelWaitableTimer(g_hTimer); } } int wmain() { g_hTimer = ::CreateWaitableTimerW(nullptr, TRUE, L"MyTimer"); assert(g_hTimer); // 设置相对超时,立即触发,周期100ms LARGE_INTEGER dueTime{}; dueTime.QuadPart = -100000; // 10ms(单位为100纳秒) assert(::SetWaitableTimer(g_hTimer, &dueTime, 100, Triggered, nullptr, FALSE)); // 持续处于可警报状态,处理APC直到定时器被取消 while(true) { DWORD ret = SleepEx(INFINITE, TRUE); if(ret != WAIT_IO_COMPLETION) { break; } } CloseHandle(g_hTimer); return 0; }
这段代码会输出10次TRIGGERED!,因为每次APC被处理后,定时器的下一次到期会继续排入新的APC,线程持续在可警报状态处理它们。
内容的提问来源于stack exchange,提问作者bugger

