65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用 SetTimer() 回调到非静态成员函数

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.38/5 (18投票s)

2003年8月21日

GPL3

4分钟阅读

viewsIcon

213181

本文介绍如何使用 SetTimer() 并以非静态成员函数作为回调。

快速更新...

在审阅了几个人的评论和建议后,我对解决方案进行了改进。请关注本文的更新,其中将使用一种更好的方法,即使用以下函数:

  • CreateWaitableTimer()
  • SetWaitableTimer()
  • WaitForMultipleObjects()

基于这些函数构建的解决方案将允许 CSleeperThread 类的多个实例运行(而当前示例只允许一个)。所以敬请关注,我将尽快更新本文。 :-)

引言

我在论坛上看到很多关于如何正确使用 SetTimer() 的问题。我也注意到,这些问题大多围绕如何让线程睡眠 X 秒。一个显而易见的答案是使用 Sleep() 函数。主要的缺点是,如何优雅地关闭线程,或者在时间到期之前取消 Sleep() 操作。

本文旨在解决以上所有问题。我提供了一个使用 SetTimer() 让线程睡眠的示例。SetTimer() 调用一个非静态函数。这一点很重要,因为通常你必须将一个静态成员传递给 SetTimer(),这意味着它无法访问类的任何其他非静态变量或成员函数。

详细说明

由于实现非静态回调成员函数是关键,我们将首先介绍这一点。实现对静态成员函数的调用,与实现常规 C 回调函数不需要任何不同。由于静态成员函数具有与 C 函数相同的签名和相同的调用约定,因此它们可以仅使用函数名来引用。

实现非静态回调成员函数则有所不同,因为它们的签名与 C 函数不同。要实现非静态成员函数,需要使用另外两项:

  • 一个全局 (void*) 指针,指向回调函数的类
  • 一个将被传递给 SetTimer() 的包装函数

这实际上是一个相当简单的实现。首先,你需要定义你的类:

class CSleeperThread : public CWinThread {
public:
  static VOID CALLBACK TimerProc_Wrapper( HWND hwnd, UINT uMsg, 
                                  UINT idEvent, DWORD dwTime );
  VOID CALLBACK TimerProc( HWND hwnd, 
                       UINT uMsg, UINT idEvent, DWORD dwTime );
  void ThreadMain();
  void WakeUp();
private:
  static void * pObject;
  UINT_PTR pTimer;
  CRITICAL_SECTION lock;
};

然后,别忘了在类实现文件中添加以下行:

void * CSleeperThread::pObject;

现在我们已经声明了类,我们可以看一下包装函数、非静态成员函数以及将要调用 SetTimer() 的成员函数。

VOID CALLBACK CSleeperThread::TimerProc_Wrapper( HWND hwnd, UINT uMsg, 
                                           UINT idEvent, DWORD dwTime ) {
 CSleeperThread *pSomeClass = (CSleeperThread*)pObject; // cast the void pointer
 pSomeClass->TimerProc(hwnd, uMsg, idEvent, dwTime); // call non-static function
}

包装函数首先使用 pObject 初始化一个 CSleeperThread 指针。由于 pSomeClass 是一个局部指针,我们可以在静态包装函数内访问它。

VOID CALLBACK CSleeperThread::TimerProc(HWND hwnd, 
     UINT uMsg, UINT idEvent, DWORD dwTime) {
 ::EnterCriticalSection(&lock);
 if(idEvent == pTimer) {
   KillTimer(NULL, pTimer);  // kill the timer so it won't fire again
   ResumeThread();  // resume the main thread function
 }
 ::LeaveCriticalSection(&lock);
}

TimerProc 成员函数不是静态的,因此我们可以访问 ResumeThread() 等其他非静态函数,也可以访问私有变量 lock。请注意,我进入了一个临界区,以防止第二个计时器事件进入回调,从而确保 TimerProc() 的第一次执行将取消计时器。

接下来,我们来看主执行函数 ThreadMain()

void CSleeperThread::ThreadMain()
{
  pObject = this; // VERY IMPORTANT, must be initialized before
                  // calling SetTimer()
  // call SetTimer, passing the wrapper function as the callback
  pTimer = SetTimer(NULL, NULL, 10000, TimerProc_Wrapper);
  // suspend until the timer expires
  SuspendThread();
  // the timer has expired, continue processing 
}

ThreadMain() 中的第一步至关重要。我们需要将类实例指针(this)赋值给 pObject 变量。这就是包装回调函数如何获得访问权限以执行非静态成员函数的方式。

接下来,我们只需调用 SetTimer(),并将一个函数指针传递给我们的包装函数。当计时器到期时,SetTimer() 将调用包装函数。然后,包装函数通过访问静态变量 pSomeClass 来执行非静态函数 TimerProc()

注意: 我选择实现一个创建计时器、进入休眠、继续处理然后完成后退出的主函数。这实际上是一个每个计时器事件只执行一次的函数。你可以在 ThreadMain() 中轻松添加一个循环,该循环将为每个计时器事件执行一次。

还有一个小函数。由于我们在 ThreadMain() 中使用了 SuspendThread(),如果我们需要唤醒线程(出于任何原因),我们只需要调用 ResumeThread()。所以,我添加了一个访问函数,如下所示:

void WakeUp() {
  ::EnterCriticalSection(&lock);
  KillTimer(NULL, pTimer);
  ResumeThread(); // wake the thread up
}

好了,各位,就到这里了...

这样,我们就得到了一个线程安全的类,它使用 SetTimer() 和非静态回调函数进行休眠;并且还能够在计时器到期前唤醒。

希望你觉得这有帮助。我已经在目前的一个项目中实际使用了这段代码,并希望其他人也能从中受益。

有人曾经告诉我:“如果你喜欢一遍又一遍地用头撞墙,你就会喜欢编程。”我发现这句话是真的,我花了整整几天时间才弄明白我在这篇文章中写的东西,我猜我就是比较慢。

呼,我头疼了,该吃点止痛药了……或者布洛芬……或者阿司匹林……或者别的什么。

鸣谢...

在撰写本文的过程中,我可能学到了很多东西。非常感谢 Lars Haendel 创建了一个专门用于理解函数指针的网站,没有它,我什么都不知道。

www.function-pointer.org.

© . All rights reserved.