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 会失败。