代码之家  ›  专栏  ›  技术社区  ›  Steve Folly

createTimerQueueTimer回调和竞争条件

  •  5
  • Steve Folly  · 技术社区  · 16 年前

    我在我的应用程序中使用定时器队列,并把一个指针指向我自己的C++定时器对象中的一个,作为回调的“参数”(在CeaTimeRealQueQueTime2中)。然后在回调中调用对象的虚拟方法。

    Timer对象的析构函数将确保使用deleteTimerQueueTimer()取消计时器。

    static void callback( PVOID param, BOOLEAN timerOrWaitFired )
    {
        Timer* timer = reinterpret_cast< Timer* >( param );
        timer->TimedOut();
    }
    
    class Timer
    {
    public:
       Timer();
    
       virtual ~Timer()
       {
           ::DeleteTimerQueueTimer( handle );
       }
    
       void Start( double period )
       {
          ::CreateTimerQueueTimer( &handle, ..., &callback, this, ... );
       }
    
       virtual void TimedOut() = 0;
    
       ...
    };
    

    但是,有一个微妙的竞争条件,如果已经调用了回调,但是Timer对象被破坏了 之前 对timedout()的调用,应用程序崩溃,因为回调调用了不存在的对象上的虚拟方法。或者更糟的是,当它被删除的时候。

    我确实有互斥锁来控制多线程调用,但我仍然遇到问题。

    使用对象指针作为回调参数真的是个好主意吗?在线程之间没有同步保证的情况下,我觉得很难闻。

    有更好的解决办法吗?其他人做什么?

    发生的一件事是保留一组指向每个计时器实例的指针(外接程序构造函数,在析构函数中移除)。但我认为这不起作用,因为如果timer是从中派生的,我们只会从基类析构函数的集合中移除指针;如果我们已经开始销毁派生对象,则已经造成了损害。

    干杯。

    3 回复  |  直到 12 年前
        1
  •  3
  •   jpalecek    16 年前

    使用对象指针作为回调函数参数的概念本身就不错。但是,显然需要在最后一个回调退出后开始销毁。

    所以,我不会把计时器抽象化,从中派生出来。我会用另一个抽象类 TimerImpl 并使 Timer 类使用 时间继电器 实例:

    class Timer
    {
      TimerInstance* impl;
      void TimeOut() { impl->TimeOut(); }
    public:
      ~Timer() {
        ... make sure the timer has ended and wont fire again after this line...
        delete impl;
      }
    }
    
    struct TimerImpl
    {
      virtual void TimeOut()=0;
      virtual ~TimerImpl();
    }
    

    这样,你就可以确保毁灭不会在你说了之后才开始。

    第二件事是,你必须等待最后一个计时器事件耗尽。根据 MSDN doc ,你可以打电话

    DeleteTimerQueueTimer(TimerQueue, Timer, INVALID_HANDLE_VALUE)
    
        2
  •  2
  •   quixver    12 年前

    调用DeleteTimerQueueTimer时,请确保传递的句柄值无效 对于完成事件。这将阻塞析构函数,直到所有挂起的回调完成或取消。

    例如

    virtual ~Timer()
       {
           ::DeleteTimerQueueTimer( timerQueue, handle, INVALID_HANDLE_VALUE );
       }
    

    这意味着您的代码将阻塞,直到所有计时器回调完成或取消。你可以像往常一样继续破坏。但有一点需要注意:不能从同一个计时器回调调用deleteTimerQueueTimer,否则将死锁。

    我相信这本身就足以防止你所经历的种族状况。

        3
  •  0
  •   CB Bailey    16 年前

    对于继承模型,您几乎肯定不能这样做。主要问题是,在输入基类构造函数时,派生对象已经无效,但计时器可能会触发,并且没有任何东西阻止它尝试虚拟函数调用,这将导致未定义的行为。

    我认为这样做的方法就是这样包装。关键是要确保尝试发送“超时”事件时不存在竞争条件。

    这个实现仍然有一个缺陷。当Timer对象开始被删除时,A Timer事件可能正在等待。析构函数可以释放互斥量,然后在计时器线程等待互斥量时销毁互斥量。我们已经阻止了“超时”事件分派中的竞争,但是线程等待被破坏的互斥体的行为取决于互斥体的实现。

    static void callback( PVOID param, BOOLEAN timerOrWaitFired );
    
    class TimerWrapper
    {
        public:
    
            /* Take reference to std::auto_ptr to ensure ownership transfer is explicit */
            TimerWrapper( std::auto_ptr<Timer>& timer ) : timer_(timer)
            {
                ::CreateTimerQueueTimer( &htimer_, ..., callback, this, ... );
            }
    
            void TimedOut()
            {
                ScopedGuard guard( mutex_ );
                if( timer_.get() )
                    timer_->TimedOut();
            }
    
            ~TimerWrapper()
            {
                ::DeleteTimerQueueTimer( htimer_, ... );
                ScopedGuard guard( mutex_ );
                timer_.reset();
            }
    
        private:
    
            Mutex mutex_;
            std::auto_ptr<Timer> timer_;
            HANDLE htimer_;
    };
    
    static void callback( PVOID param, BOOLEAN timerOrWaitFired )
    {
        TimerWrapper* timer = reinterpret_cast< TimerWrapper* >( param );
        timer->TimedOut();
    }