Как безопасно реализовать шаблон наблюдателя? - PullRequest
3 голосов
/ 09 февраля 2011

Я реализую механизм, похожий на шаблон проектирования наблюдателя для многопоточной игры в тетрис.Существует класс Game, который содержит коллекцию объектов EventHandler.Если класс хочет зарегистрировать себя в качестве слушателя объекта Game, он должен наследовать класс Game :: EventHandler.При событиях изменения состояния соответствующий интерфейс вызывается на интерфейсе EventHandler каждого слушателя.Вот как выглядит код:

class Game
{
public:
    class EventHandler
    {
    public:
        EventHandler();

        virtual ~EventHandler();

        virtual void onGameStateChanged(Game * inGame) = 0;

        virtual void onLinesCleared(Game * inGame, int inLineCount) = 0;

    private:
        EventHandler(const EventHandler&);
        EventHandler& operator=(const EventHandler&);
    };

    static void RegisterEventHandler(ThreadSafe<Game> inGame, EventHandler * inEventHandler);

    static void UnregisterEventHandler(ThreadSafe<Game> inGame, EventHandler * inEventHandler);

    typedef std::set<EventHandler*> EventHandlers;
    EventHandlers mEventHandlers;

private:    
    typedef std::set<Game*> Instances;
    static Instances sInstances;
};


void Game::RegisterEventHandler(ThreadSafe<Game> inGame, EventHandler * inEventHandler)
{
    ScopedReaderAndWriter<Game> rwgame(inGame);
    Game * game(rwgame.get());
    if (sInstances.find(game) == sInstances.end())
    {
        LogWarning("Game::RegisterEventHandler: This game object does not exist!");
        return;
    }

    game->mEventHandlers.insert(inEventHandler);
}


void Game::UnregisterEventHandler(ThreadSafe<Game> inGame, EventHandler * inEventHandler)
{
    ScopedReaderAndWriter<Game> rwgame(inGame);
    Game * game(rwgame.get());
    if (sInstances.find(game) == sInstances.end())
    {
        LogWarning("Game::UnregisterEventHandler: The game object no longer exists!");
        return;
    }

    game->mEventHandlers.erase(inEventHandler);
}

С этим шаблоном я часто сталкиваюсь с двумя проблемами:

  1. Объект слушателя хочет отменить регистрацию в ужеудаленный объект, приводящий к сбою.
  2. Событие запускается для слушателя, который больше не существует.Это чаще всего происходит в многопоточном коде.Вот типичный сценарий:
    • Состояние игры изменяется в рабочем потоке.Мы хотим, чтобы уведомление происходило в главном потоке.
    • Событие помещается в boost :: function и отправляется в виде PostMessage в основной поток.
    • Через некоторое время этот объект функцииобрабатывается основным потоком, в то время как объект Game уже удален.В результате происходит сбой.

Мой текущий обходной путь - тот, который вы можете увидеть в приведенном выше примере кода.Я сделал UnregisterEventHandler статическим методом, который проверяет список экземпляров.Это помогает, но я нахожу это несколько хакерским решением.

Кто-нибудь знает набор рекомендаций о том, как правильно и безопасно внедрить систему уведомлений / прослушивателей?Любой совет, как избежать вышеупомянутых ловушек?

PS: Если вам нужна дополнительная информация, чтобы ответить на этот вопрос, вы можете найти соответствующий код онлайн здесь: Game.h , Game.cpp , SimpleGame.h , SimpleGame.cpp , MainWindow.cpp .

Ответы [ 2 ]

1 голос
/ 09 февраля 2011
  1. Практическое правило заключается в том, что delete и new для объекта должны находиться рядом друг с другом.Например, в конструкторе и деструкторе или до и после вызова, когда вы используете объект.Поэтому плохой практикой является удаление объекта в другом объекте, если последний не создал прежний.

  2. Я не понимаю, как вы упаковываете события.Похоже, вам нужно проверить, игра еще жива, прежде чем обрабатывать событие.Или вы можете использовать shared_ptr в событиях и других местах, чтобы убедиться, что игры удаляются последними.

0 голосов
/ 13 ноября 2013

Я пишу много кода на C ++ и мне нужно было создать Observer для некоторых игровых компонентов, над которыми я работал. Мне нужно было что-то, чтобы распространять «начало кадра», «пользовательский ввод» и т. Д. В качестве событий в игре среди заинтересованных сторон. У меня была такая же проблема, чтобы рассмотреть ... запуск события привел бы к уничтожению другого наблюдателя, который также может впоследствии выстрелить. Мне нужно справиться с этим. Мне не нужно было заботиться о безопасности потоков, но требования к дизайну, за которые я обычно берусь, - это создать что-то достаточно простое (с точки зрения API), чтобы я мог установить несколько мьютексов в нужном месте, а остальное должно позаботиться о себе.

Я также хотел, чтобы это был прямой C ++, не зависящий от платформы или конкретной технологии (такой как boost, Qt и т. Д.), Потому что я часто создаю и повторно использую компоненты (и идеи, лежащие в их основе) в разных проектах. .

Вот примерный набросок того, что я придумал как решение:

  1. Observer - это одноэлементный файл с ключами (перечисляемыми значениями, а не строками) для субъектов, в которых регистрируется интерес. Поскольку он является одноэлементным, он всегда существует.
  2. Каждый предмет является производным от общего базового класса. Базовый класс имеет абстрактную виртуальную функцию Notify (...), которая должна быть реализована в производных классах, и деструктор, который удаляет его из Observer (которого он всегда может достичь) при удалении.
  3. Внутри самого Обозревателя, если Detach (...) вызывается во время выполнения Уведомления (...), все отсоединенные субъекты оказываются в списке.
  4. Когда в Observer вызывается Notify (...), он создает временную копию списка тем. Поскольку это повторяется по этому, это сравнивает это с недавно отделенным. Если цель не указана, для нее вызывается уведомление (...). В противном случае оно пропускается.
  5. Notify (...) в Observer также отслеживает глубину обработки каскадных вызовов (A уведомляет B, C, D, а D.Notify (...) инициирует вызов Notify (...) к E и т. д.)

Вот как в итоге выглядел интерфейс:

/* 
 The Notifier is a singleton implementation of the Subject/Observer design
 pattern.  Any class/instance which wishes to participate as an observer
 of an event can derive from the Notified base class and register itself
 with the Notiifer for enumerated events.

 Notifier derived classes MUST implement the notify function, which has 
 a prototype of:

 void Notify(const NOTIFIED_EVENT_TYPE_T& event)

 This is a data object passed from the Notifier class.  The structure 
 passed has a void* in it.  There is no illusion of type safety here 
 and it is the responsibility of the user to ensure it is cast properly.
 In most cases, it will be "NULL".

 Classes derived from Notified do not need to deregister (though it may 
 be a good idea to do so) as the base class destrctor will attempt to
 remove itself from the Notifier system automatically.

 The event type is an enumeration and not a string as it is in many 
 "generic" notification systems.  In practical use, this is for a closed
 application where the messages will be known at compile time.  This allows
 us to increase the speed of the delivery by NOT having a 
 dictionary keyed lookup mechanism.  Some loss of generality is implied 
 by this.

 This class/system is NOT thread safe, but could be made so with some
 mutex wrappers.  It is safe to call Attach/Detach as a consequence 
 of calling Notify(...).  

 */


class Notified;

class Notifier : public SingletonDynamic<Notifier>
{
public:
   typedef enum
   {
      NE_MIN = 0,
      NE_DEBUG_BUTTON_PRESSED = NE_MIN,
      NE_DEBUG_LINE_DRAW_ADD_LINE_PIXELS,
      NE_DEBUG_TOGGLE_VISIBILITY,
      NE_DEBUG_MESSAGE,
      NE_RESET_DRAW_CYCLE,
      NE_VIEWPORT_CHANGED,
      NE_MAX,
   } NOTIFIED_EVENT_TYPE_T;

private:
   typedef vector<NOTIFIED_EVENT_TYPE_T> NOTIFIED_EVENT_TYPE_VECTOR_T;

   typedef map<Notified*,NOTIFIED_EVENT_TYPE_VECTOR_T> NOTIFIED_MAP_T;
   typedef map<Notified*,NOTIFIED_EVENT_TYPE_VECTOR_T>::iterator NOTIFIED_MAP_ITER_T;

   typedef vector<Notified*> NOTIFIED_VECTOR_T;
   typedef vector<NOTIFIED_VECTOR_T> NOTIFIED_VECTOR_VECTOR_T;

   NOTIFIED_MAP_T _notifiedMap;
   NOTIFIED_VECTOR_VECTOR_T _notifiedVector;
   NOTIFIED_MAP_ITER_T _mapIter;

   // This vector keeps a temporary list of observers that have completely
   // detached since the current "Notify(...)" operation began.  This is
   // to handle the problem where a Notified instance has called Detach(...)
   // because of a Notify(...) call.  The removed instance could be a dead
   // pointer, so don't try to talk to it.
   vector<Notified*> _detached;
   int32 _notifyDepth;

   void RemoveEvent(NOTIFIED_EVENT_TYPE_VECTOR_T& orgEventTypes, NOTIFIED_EVENT_TYPE_T eventType);
   void RemoveNotified(NOTIFIED_VECTOR_T& orgNotified, Notified* observer);

public:

   virtual void Reset();
   virtual bool Init() { Reset(); return true; }
   virtual void Shutdown() { Reset(); }

   void Attach(Notified* observer, NOTIFIED_EVENT_TYPE_T eventType);
   // Detach for a specific event
   void Detach(Notified* observer, NOTIFIED_EVENT_TYPE_T eventType);
   // Detach for ALL events
   void Detach(Notified* observer);

   /* The design of this interface is very specific.  I could 
    * create a class to hold all the event data and then the
    * method would just have take that object.  But then I would
    * have to search for every place in the code that created an
    * object to be used and make sure it updated the passed in
    * object when a member is added to it.  This way, a break
    * occurs at compile time that must be addressed.
    */
   void Notify(NOTIFIED_EVENT_TYPE_T, const void* eventData = NULL);

   /* Used for CPPUnit.  Could create a Mock...maybe...but this seems
    * like it will get the job done with minimal fuss.  For now.
    */
   // Return all events that this object is registered for.
   vector<NOTIFIED_EVENT_TYPE_T> GetEvents(Notified* observer);
   // Return all objects registered for this event.
   vector<Notified*> GetNotified(NOTIFIED_EVENT_TYPE_T event);
};

/* This is the base class for anything that can receive notifications.
 */
class Notified
{
public:
   virtual void Notify(Notifier::NOTIFIED_EVENT_TYPE_T eventType, const void* eventData) = 0;
   virtual ~Notified();

};

typedef Notifier::NOTIFIED_EVENT_TYPE_T NOTIFIED_EVENT_TYPE_T;

ПРИМЕЧАНИЕ. Класс Notification имеет одну функцию, здесь (Notify) ... Поскольку void * не является безопасным типом, я создал другие версии, в которых notify выглядит так:

virtual void Notify(Notifier::NOTIFIED_EVENT_TYPE_T eventType, int value); 
virtual void Notify(Notifier::NOTIFIED_EVENT_TYPE_T eventType, const string& str);

Соответствующие методы Notify (...) были добавлены в сам Notifier. Все они использовали одну функцию для получения «списка целей», а затем вызывали соответствующую функцию для целей. Это работает хорошо и удерживает приемника от необходимости делать уродливые броски.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...