【问题标题】:How to implement the observer pattern safely?如何安全地实现观察者模式?
【发布时间】:2011-06-23 17:35:33
【问题描述】:

我正在为多线程俄罗斯方块游戏实现类似于观察者设计模式的机制。有一个包含 EventHandler 对象集合的 Game 类。如果一个类想要将自己注册为一个 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 发送到主线程。
    • 不久之后,该函数对象由主线程处理,而游戏对象已被删除。结果是崩溃。

我当前的解决方法是您可以在上面的代码示例中看到的解决方法。我将UnregisterEventHandler 设为了一个静态方法,用于检查实例列表。这确实有帮助,但我觉得这是一个有点老套的解决方案。

有没有人知道一套关于如何干净、安全地实施通知器/侦听器系统的指南?关于如何避免上述陷阱的任何建议?

PS:如果您需要更多信息来回答这个问题,您可以在这里在线找到相关代码:Game.hGame.cppSimpleGame.hSimpleGame.cppMainWindow.cpp

【问题讨论】:

  • 你应该看看shared_ptr&lt;T&gt;weak_ptr&lt;T&gt;
  • 既然您已经在使用 boost,请查看 boost::signals2 来实现您的事件。它是线程安全的。我倾向于做的是触发当前线程中的事件。如果需要,由事件处理程序自己发布到主线程。对我来说,事件处理程序通常是 UI 对象,系统会负责将已发布的消息丢弃到已销毁的 UI 对象中。
  • @gngr44:我认为你的解决方案值得回答,而不是评论,这正是Boost.Signals2 的目的。

标签: c++ observer-pattern


【解决方案1】:

我编写了很多 C++ 代码,并且需要为我正在开发的一些游戏组件创建一个 Observer。我需要一些东西来将“帧开始”、“用户输入”等作为游戏中的事件分发给感兴趣的各方。我有同样的问题要考虑......事件的触发会导致另一个观察者的破坏,该观察者也可能随后触发。我需要处理这个。我不需要处理线程安全,但我通常追求的设计要求是构建足够简单(API 方面)的东西,我可以在正确的位置放置一些互斥锁,其余的应该自己处理。

我还希望它是纯 C++,不依赖于平台或特定技术(如 boost、Qt 等),因为我经常在不同的项目中构建和重用组件(以及它们背后的想法) .

这是我想出的解决方案的粗略草图:

  1. Observer 是一个带有键(枚举值,而不是字符串)的单例,供 Subjects 注册感兴趣。因为它是单例,所以它始终存在。
  2. 每个主题都派生自一个公共基类。基类有一个抽象虚函数 Notify(...),它必须在派生类中实现,以及一个析构函数,当它被删除时,它会从 Observer 中移除(它总是可以到达)。
  3. 在 Observer 本身内部,如果在 Notify(...) 正在进行时调用 Detach(...),则任何已分离的 Subjects 最终都会出现在列表中。
  4. 当在 Observer 上调用 Notify(...) 时,它会创建主题列表的临时副本。当它迭代它时,它将它与最近分离的进行比较。如果目标不在其上,则在目标上调用 Notify(...)。否则,将被跳过。
  5. Observer 中的 Notify(...) 还跟踪处理级联调用的深度(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;

注意:Notified 类只有一个函数,此处为 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(...) 方法已添加到通知程序本身。所有这些都使用一个函数来获取“目标列表”,然后在目标上调用适当的函数。这很好用,并且使接收器不必进行难看的演员表。

这似乎运作良好。该解决方案与源代码一起发布在网络here 上。这是一个相对较新的设计,因此非常感谢任何反馈。

【讨论】:

    【解决方案2】:
    1. 经验法则是对象的 delete 和 new 应该彼此靠近。例如。在构造函数和析构函数中,或者在使用对象的调用之前和之后。因此,当另一个对象没有创建前一个对象时,删除另一个对象中的一个对象是一种不好的做法。

    2. 我不明白你是如何打包活动的。在处理事件之前,您似乎必须检查游戏是否仍然存在。或者您可以在活动和其他地方使用shared_ptr,以确保最后删除游戏。

    【讨论】:

      猜你喜欢
      • 2011-12-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-02-12
      • 2013-10-28
      • 1970-01-01
      • 2016-02-21
      相关资源
      最近更新 更多