Windows SleepEx() doesn't resume when queueing an APC
I have the problem that a thread that was sent to sleep using SleepEx(INFINITE, true)
does not reliably continue when an APC is queued.
The application scenario is that software A must notice when software B has installed a certain Windows service.
For this, I create a new thread, register a callback function via NotifyServiceStatusChange()
, and put the thread to sleep via SleepEx(INFINITE, true)
.
If, during the runtime of software A, the specified service is installed, the callback function is called, the thread is continued, and finishes its run()
method. Everything works fine.
But, if software A terminates without the callback function being called, I still want the thread to terminate properly.
The Microsoft documentation states this about the SleepEx
function:
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.
Therefore, I queue an APC to my thread using QueueUserAPC()
. This works fine, my function stopSleeping()
is called and executed: A breakpoint can be reached and debug output can be made inside this function.
Unfortunately, contrary to my expectation, my own APC does not cause the thread to resume running, as the call of the function callback()
does.
The question is, why not?
My thread is a class derived from QThread
and the stopThread()
method is triggered by a SIGNAL/SLOT connection from the main thread.
// **********************************************
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 );
}
I'm not 100% sure on this, given that you didn't provide a runnable example it's hard to verify.
My guess is that you're scheduling the APC on the main thread, instead of the CCheckForService
thread.
If CCheckForService::stopThread
is called from a signal/slot on the main thread, then it'll execute on the main thread.
So ::GetCurrentThreadId()
will return the thread id of the main thread, and you subsequently end up with calling QueueUserAPC()
with a thread handle for the main thread, so the APC will execute on the main thread.
So the CCheckForService
will remain sleeping, because it never received an APC.
You can verify this by comparing QApplication::instance()->thread()
with QThread::currentThread()
inside your CCheckForService::stopSleeping
method - if they're equal you scheduled the APC on the main thread instead of the worker.
There's unfortunately no officially supported way in QT to get the thread id / thread handle of a QThread
, apart from calling QThread::currentThreadId()
.
So you'd have to store the thread id in your CCheckForService
class, so you can later get the appropriate thread handle, e.g.:
// 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 );
}
There's still a small race condition left in this example though - if you call stopThread()
before the thread has started up & set the runningThreadId
. In that case OpenThread()
will fail and return NULL
, so queuing the APC will fail.