Обратный вызов CreateTimerQueueTimer и состояние гонки - PullRequest
5 голосов
/ 25 января 2009

Я использую очереди таймеров в своем приложении и передаю указатель на один из моих собственных объектов таймера C ++ в качестве «параметра» для обратного вызова (в CreateTimerQueueTimer). Затем я вызываю виртуальный метод для объекта в обратном вызове.

Деструктор объекта 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;

   ...
};

Однако существует тонкое условие гонки: если обратный вызов уже был вызван, но объект таймера уничтожен до вызова TimedOut (), приложение завершает работу, потому что обратный вызов вызывает виртуальный метод на несуществующий объект. Или еще хуже, пока он удаляется.

У меня есть мьютексы для управления многопоточными вызовами, но проблема все еще возникает.

Является ли использование указателя объекта в качестве параметра обратного вызова действительно хорошей идеей? Без каких-либо гарантий синхронизации между потоками он просто пахнет плохо.

Есть ли лучшее решение? Что делают другие люди?

Единственное, что происходит, - это сохранить набор указателей на каждый экземпляр Timer (добавить в конструктор, удалить в деструктор). Но я не думаю, что это сработает, потому что если Timer получен из, мы удалили бы только указатель из набора в деструкторе базового класса; ущерб уже нанесен, если мы начали уничтожать производный объект.

Приветствие.

Ответы [ 3 ]

3 голосов
/ 25 января 2009

Концепция использования указателя объекта в качестве параметра функции обратного вызова сама по себе неплоха. Однако вам, очевидно, нужно начать уничтожение после завершения последнего обратного вызова.

Итак, я бы не стал делать абстрактные таймеры и вообще ничего не извлекать из них. Я бы использовал другой абстрактный класс TimerImpl, а класс Timer использовал бы экземпляр TimerImpl:

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 голосов
/ 05 апреля 2011

Когда вы вызываете DeleteTimerQueueTimer, убедитесь, что вы передали INVALID_HANDLE_VALUE для завершения события. Это будет блокировать ваш деструктор до тех пор, пока все ожидающие обратные вызовы не будут завершены или отменены.

например.

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

Это означает, что ваш код будет блокироваться, пока все обратные вызовы таймера не будут завершены или отменены. Затем вы можете продолжить уничтожение как обычно. Однако следует отметить одну вещь - вы не можете вызывать deletetimerqueuetimer из того же обратного вызова таймера, иначе вы будете в тупике.

Я считаю, что одного этого должно быть достаточно, чтобы предотвратить состояние расы, с которым вы столкнулись.

0 голосов
/ 25 января 2009

Вы почти наверняка не можете сделать это с моделью наследования. Основная проблема заключается в том, что к моменту ввода конструктора базового класса производный объект уже недействителен, но таймер может сработать, и ничто не остановит его при попытке вызова виртуальной функции, что теперь приведет к неопределенному поведению.

Я думаю, что способ сделать это - обертка как это. Смысл в том, чтобы при попытке отправить событие «Тайм-аут» не было условий гонки.

Эта реализация все еще имеет один недостаток. Существует вероятность того, что событие таймера ожидает, когда объект таймера начинает удаляться. Деструктор может освободить мьютекс, а затем уничтожить мьютекс, пока поток таймера ожидает мьютекса. Мы предотвратили гонку при отправке события «timed out», но поведение потока, ожидающего мьютекс, который уничтожается, зависит от реализации мьютекса.

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();
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...