Windows 排队 APC 时 SleepEx() 不会恢复
Windows SleepEx() doesn't resume when queueing an APC
我遇到的问题是,当 APC 排队时,使用 SleepEx(INFINITE, true)
进入睡眠状态的线程无法可靠地继续。
应用场景是当软件B安装了某个Windows服务时软件A必须通知。
为此,我创建了一个新线程,通过NotifyServiceStatusChange()
注册了一个回调函数,并通过SleepEx(INFINITE, true)
.
让线程休眠
如果在软件A运行期间安装了指定的服务,则调用回调函数,继续线程,并完成其run()
方法。一切正常。
但是,如果软件 A 在没有调用回调函数的情况下终止,我仍然希望线程正确终止。
Microsoft 文档对 SleepEx
函数进行了说明:
Execution resumes when one of the following occurs:
- An I/O completion callback function is called.
- An asynchronous procedure call (APC) is queued to the thread.
- The time-out interval elapses.
因此,我使用 QueueUserAPC()
将 APC 排队到我的线程。这工作正常,我的函数 stopSleeping()
被调用并执行:可以到达断点并可以在该函数内部进行调试输出。
不幸的是,与我的预期相反,我自己的 APC 并没有像调用函数 callback()
那样导致线程恢复 运行。
问题是,为什么不呢?
我的线程是从 QThread
派生的 class 并且 stopThread()
方法由来自主线程的 SIGNAL/SLOT 连接触发。
// **********************************************
void CCheckForService::run()
{
SC_HANDLE SCHandle = ::OpenSCManager( 0
, SERVICES_ACTIVE_DATABASE
, SC_MANAGER_ENUMERATE_SERVICE
);
if ( 0 != SCHandle )
{
meStatus = Status::BEFORE_NOTIFY_SVC_CHANGE;
SERVICE_STATUS_PROCESS ssp;
char text[] = "MyServiceToLookFor";
wchar_t wtext[ 20 ];
mbstowcs( wtext, text, strlen( text ) + 1 );
LPWSTR lpWText = wtext;
SERVICE_NOTIFY serviceNotify = { SERVICE_NOTIFY_STATUS_CHANGE
, &CCheckForIIoT::callback
, nullptr
, 0
, ssp
, 0
, lpWText
};
// Callback function is to be invoked if "MyServiceToLookFor" has been installed
const DWORD result = ::NotifyServiceStatusChange( SCHandle
, SERVICE_NOTIFY_CREATED
, &serviceNotify
);
if ( ERROR_SUCCESS == result )
{
meStatus = Status::WAITING_FOR_CALLBACK;
::SleepEx( INFINITE, true ); // Wait for the callback function
}
LocalFree( lpWText );
}
::CloseServiceHandle( SCHandle );
if ( Status::CANCELLED != meStatus )
{
// inform main thread
emit sendReady( meStatus );
}
}
// **********************************************
// [static]
void CCheckForService::stopSleeping( ULONG_PTR in )
{
Q_UNUSED( in )
meStatus = Status::CANCELLED;
}
// **********************************************
// [static]
void CCheckForService::callback( void* apParam )
{
auto lpServiceNotify = static_cast< SERVICE_NOTIFY* >( apParam );
// the service is now installed; now wait until it runs
{
QtServiceController lBrokerService( "MyServiceToLookFor" );
QTime WaitTime;
WaitTime.start();
while ( !lBrokerService.isRunning() )
{
msleep( 1000 );
// Timeout check
if ( WaitTime.elapsed() > WAIT_FOR_SERVICE_RUN * 1000 )
{
break;
}
}
}
meStatus = Status::OK;
}
// **********************************************
// [SLOT]
void CCheckForService::stopThread( void )
{
HANDLE ThreadHandle( ::OpenThread( THREAD_ALL_ACCESS
, true
, ::GetCurrentThreadId()
)
);
DWORD d = ::QueueUserAPC( &CCheckForIIoT::stopSleeping
, ThreadHandle
, NULL
);
::CloseHandle( ThreadHandle );
}
我不是 100% 确定这一点,因为您没有提供可运行的示例,因此很难验证。
我猜你是在主线程上安排 APC,而不是 CCheckForService
线程。
如果 CCheckForService::stopThread
在主线程上从 signal/slot 调用,那么它将在主线程上执行。
因此 ::GetCurrentThreadId()
将 return 主线程的线程 ID,随后您最终调用 QueueUserAPC()
主线程的线程句柄,因此 APC 将在主线程上执行。
因此 CCheckForService
将保持休眠状态,因为它从未收到 APC。
您可以通过在 CCheckForService::stopSleeping
方法中比较 QApplication::instance()->thread()
和 QThread::currentThread()
来验证这一点 - 如果它们相等,则您将 APC 安排在主线程而不是工作线程上。
不幸的是,除了调用 QThread::currentThreadId()
.
之外,QT 中没有官方支持的方法来获取 QThread
的线程 ID/线程句柄
因此您必须将线程 ID 存储在 CCheckForService
class 中,以便您稍后可以获取适当的线程句柄,例如:
// TODO: Add to CCheckForService declaration
// private: DWORD runningThreadId;
// TODO: initialize runningThreadId in constructor to 0
// 0 is guaranteed to never be a valid thread id
void CCheckForService::run() {
runningThreadId = ::GetCurrentThreadId();
/** ... Rest of original run() ... **/
}
void CCheckForService::stopThread( void )
{
HANDLE ThreadHandle( ::OpenThread( THREAD_ALL_ACCESS
, true
, runningThreadId /* <------ */
)
);
DWORD d = ::QueueUserAPC( &CCheckForIIoT::stopSleeping
, ThreadHandle
, NULL
);
::CloseHandle( ThreadHandle );
}
虽然在这个例子中仍然存在一个小的竞争条件 - 如果您在线程启动之前调用 stopThread()
并设置 runningThreadId
。在那种情况下 OpenThread()
会失败,return NULL
,所以排队 APC 会失败。
我遇到的问题是,当 APC 排队时,使用 SleepEx(INFINITE, true)
进入睡眠状态的线程无法可靠地继续。
应用场景是当软件B安装了某个Windows服务时软件A必须通知。
为此,我创建了一个新线程,通过NotifyServiceStatusChange()
注册了一个回调函数,并通过SleepEx(INFINITE, true)
.
如果在软件A运行期间安装了指定的服务,则调用回调函数,继续线程,并完成其run()
方法。一切正常。
但是,如果软件 A 在没有调用回调函数的情况下终止,我仍然希望线程正确终止。
Microsoft 文档对 SleepEx
函数进行了说明:
Execution resumes when one of the following occurs:
- An I/O completion callback function is called.
- An asynchronous procedure call (APC) is queued to the thread.
- The time-out interval elapses.
因此,我使用 QueueUserAPC()
将 APC 排队到我的线程。这工作正常,我的函数 stopSleeping()
被调用并执行:可以到达断点并可以在该函数内部进行调试输出。
不幸的是,与我的预期相反,我自己的 APC 并没有像调用函数 callback()
那样导致线程恢复 运行。
问题是,为什么不呢?
我的线程是从 QThread
派生的 class 并且 stopThread()
方法由来自主线程的 SIGNAL/SLOT 连接触发。
// **********************************************
void CCheckForService::run()
{
SC_HANDLE SCHandle = ::OpenSCManager( 0
, SERVICES_ACTIVE_DATABASE
, SC_MANAGER_ENUMERATE_SERVICE
);
if ( 0 != SCHandle )
{
meStatus = Status::BEFORE_NOTIFY_SVC_CHANGE;
SERVICE_STATUS_PROCESS ssp;
char text[] = "MyServiceToLookFor";
wchar_t wtext[ 20 ];
mbstowcs( wtext, text, strlen( text ) + 1 );
LPWSTR lpWText = wtext;
SERVICE_NOTIFY serviceNotify = { SERVICE_NOTIFY_STATUS_CHANGE
, &CCheckForIIoT::callback
, nullptr
, 0
, ssp
, 0
, lpWText
};
// Callback function is to be invoked if "MyServiceToLookFor" has been installed
const DWORD result = ::NotifyServiceStatusChange( SCHandle
, SERVICE_NOTIFY_CREATED
, &serviceNotify
);
if ( ERROR_SUCCESS == result )
{
meStatus = Status::WAITING_FOR_CALLBACK;
::SleepEx( INFINITE, true ); // Wait for the callback function
}
LocalFree( lpWText );
}
::CloseServiceHandle( SCHandle );
if ( Status::CANCELLED != meStatus )
{
// inform main thread
emit sendReady( meStatus );
}
}
// **********************************************
// [static]
void CCheckForService::stopSleeping( ULONG_PTR in )
{
Q_UNUSED( in )
meStatus = Status::CANCELLED;
}
// **********************************************
// [static]
void CCheckForService::callback( void* apParam )
{
auto lpServiceNotify = static_cast< SERVICE_NOTIFY* >( apParam );
// the service is now installed; now wait until it runs
{
QtServiceController lBrokerService( "MyServiceToLookFor" );
QTime WaitTime;
WaitTime.start();
while ( !lBrokerService.isRunning() )
{
msleep( 1000 );
// Timeout check
if ( WaitTime.elapsed() > WAIT_FOR_SERVICE_RUN * 1000 )
{
break;
}
}
}
meStatus = Status::OK;
}
// **********************************************
// [SLOT]
void CCheckForService::stopThread( void )
{
HANDLE ThreadHandle( ::OpenThread( THREAD_ALL_ACCESS
, true
, ::GetCurrentThreadId()
)
);
DWORD d = ::QueueUserAPC( &CCheckForIIoT::stopSleeping
, ThreadHandle
, NULL
);
::CloseHandle( ThreadHandle );
}
我不是 100% 确定这一点,因为您没有提供可运行的示例,因此很难验证。
我猜你是在主线程上安排 APC,而不是 CCheckForService
线程。
如果 CCheckForService::stopThread
在主线程上从 signal/slot 调用,那么它将在主线程上执行。
因此 ::GetCurrentThreadId()
将 return 主线程的线程 ID,随后您最终调用 QueueUserAPC()
主线程的线程句柄,因此 APC 将在主线程上执行。
因此 CCheckForService
将保持休眠状态,因为它从未收到 APC。
您可以通过在 CCheckForService::stopSleeping
方法中比较 QApplication::instance()->thread()
和 QThread::currentThread()
来验证这一点 - 如果它们相等,则您将 APC 安排在主线程而不是工作线程上。
不幸的是,除了调用 QThread::currentThreadId()
.
QThread
的线程 ID/线程句柄
因此您必须将线程 ID 存储在 CCheckForService
class 中,以便您稍后可以获取适当的线程句柄,例如:
// TODO: Add to CCheckForService declaration
// private: DWORD runningThreadId;
// TODO: initialize runningThreadId in constructor to 0
// 0 is guaranteed to never be a valid thread id
void CCheckForService::run() {
runningThreadId = ::GetCurrentThreadId();
/** ... Rest of original run() ... **/
}
void CCheckForService::stopThread( void )
{
HANDLE ThreadHandle( ::OpenThread( THREAD_ALL_ACCESS
, true
, runningThreadId /* <------ */
)
);
DWORD d = ::QueueUserAPC( &CCheckForIIoT::stopSleeping
, ThreadHandle
, NULL
);
::CloseHandle( ThreadHandle );
}
虽然在这个例子中仍然存在一个小的竞争条件 - 如果您在线程启动之前调用 stopThread()
并设置 runningThreadId
。在那种情况下 OpenThread()
会失败,return NULL
,所以排队 APC 会失败。