如何安全地实现观察者模式?

4

我正在为一个多线程俄罗斯方块游戏实现类似观察者设计模式的机制。有一个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”设置为静态方法,它会检查实例列表。这确实有所帮助,但我认为这是一个有些hackish的解决方案。
有人知道如何干净而安全地实现通知器/监听器系统的一组准则吗?有什么建议可以避免上述问题吗?
PS:如果您需要更多信息来回答这个问题,您可以在此处在线找到相关代码:Game.hGame.cppSimpleGame.hSimpleGame.cppMainWindow.cpp

2
你应该了解一下 shared_ptr<T>weak_ptr<T> - fredoverflow
1
既然您已经在使用boost,建议您尝试使用boost::signals2来实现您的事件。它是线程安全的。我通常会在当前线程中触发事件,而由事件处理程序自己决定是否需要在主线程中发布消息。对我而言,事件处理程序通常是UI对象,系统将负责丢弃发送到已销毁的UI对象的消息。 - gngr44
@gngr44:我认为你的解决方案值得一份答案,而不是一个评论,这正是“Boost.Signals2”所设计的。 - Matthieu M.
2个回答

1
1. 经验法则是,删除对象和新建对象的位置应该靠近。例如,在构造函数和析构函数中,或使用对象的调用之前和之后。所以在一个对象中删除另一个对象是一个不好的做法,因为后者没有创建前者。
2. 我不明白你是如何打包事件的。似乎你必须在处理事件之前检查游戏是否仍然存活。或者你可以在事件和其他地方使用shared_ptr来确保游戏最后被删除。

0
我写了很多C++代码,需要为我正在开发的一些游戏组件创建一个观察者。我需要将游戏中的“帧开始”、“用户输入”等内容作为事件分发给感兴趣的方。我需要考虑同样的问题...事件的触发可能会导致另一个观察者的销毁,后者也可能随后触发。我需要处理这个问题。我不需要处理线程安全性,但我通常追求的设计要求是构建一个足够简单(在API方面)的东西,以便我可以在正确的位置放置一些互斥锁,其余的事情应该自己处理。
我还希望它是纯粹的C++,不依赖于平台或特定技术(如boost、Qt等),因为我经常在不同的项目中构建和重用组件(以及它们背后的思想)。
以下是我提出的解决方案的大致草图:
  1. 观察者模式是一个单例,具有用于注册兴趣的键(枚举值,而不是字符串)的子主题。由于它是一个单例,因此它始终存在。
  2. 每个主题都源自于一个共同的基类。该基类具有必须在派生类中实现的抽象虚函数Notify(...)和一个析构函数,在删除它时从观察者(它总是可以访问)中删除它。
  3. 在观察者本身内部,如果在Notify(...)正在进行时调用Detach(...),则任何分离的主题最终都会进入列表。
  4. 当在观察者上调用Notify(...)时,它会创建主题列表的临时副本。当它对其进行迭代时,它与最近分离的主题进行比较。如果目标没有在其中,则在目标上调用Notify(...)。否则,它将被跳过。
  5. 观察者中的Notify(...)还记录深度以处理级联调用(A通知B、C、D,D.Notify(...)触发对E的Notify(...)调用等)

这就是接口最终看起来像什么:

/* 
 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);

在 Notifier 本身中添加了相应的 Notify(...) 方法。所有这些方法都使用单个函数来获取“目标列表”,然后在目标上调用适当的函数。这样做效果很好,可以避免接收器进行丑陋的转换。

这似乎很有效。解决方案已经发布在网上这里,同时也提供了源代码。这是一个相对较新的设计,因此非常欢迎任何反馈意见。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接