在通知期间注册/注销观察员时,如何避免死锁



我有解决这个问题的想法,但我觉得这个问题已经解决了很多次了。

我实现了一个观察者模式,类似于此:

struct IObserver {
  virtual void notify(Event &event) = 0;
}
struct Notifier {
  void registerObserver(IObserver* observer, EventRange range) {
    lock(_mutex);
    _observers[observer] = range;
  }
  void deregisterObserver(IObserver* observer) {
    lock(_mutex);
    _observers.erase(observers.find(observer));
  }
  void handleEvent() { /* pushes event onto queue */ }
  void run();
  mutex _mutex;
  queue<Event> _eventQueue;
  map<IObserver, EventRange> _observers;
}

run方法是从我创建的线程中调用的(它实际上由通知程序所有)。这个方法看起来有点像。。。

void Notifier::run() {
  while(true) {
    waitForEvent();
    Event event = _eventQueue.pop();
    // now we have an event, acquire a lock and notify listeners
    lock(_mutex);
    BOOST_FOREACH(map<IObserver, EventRange>::value_type &value, _observers){
      value.first->notify(event);
    }
  }
}

这非常有效,直到notify尝试创建一个对象,而该对象又尝试注册一个观察者。在这种情况下,试图获取已经锁定的锁,结果导致死锁。这种情况可以通过使用递归互斥来避免。然而,现在考虑通知触发移除观察员的情况。现在映射迭代器无效。

我的问题是,是否有一种模式可以防止这种僵局?

我认为这里真正的问题是,在迭代观察者列表时,有一个事件在操纵观察者列表。如果您正在执行notify(…)操作,那么您正在对列表进行迭代。如果您正在对原始列表(而不是副本)进行迭代,那么在对列表进行迭代时,注册或注销都会更改列表。我认为std::map中的迭代器无法很好地处理此问题。

我也遇到过这个问题(只是在单线程上下文中),并发现处理它的唯一方法是创建一个观察者列表的临时副本并对其进行迭代。

在迭代过程中,我还缓存了删除的观察者,所以我可以确定,如果我有观察者A、B和C,那么如果A导致C被删除,列表中仍然有C,但C会被跳过。

我有一个用于单线程应用程序的实现。

您可以通过一些工作将其转换为线程化方法。

EDIT:我认为多线程应用程序的漏洞在于创建观察者列表的副本(当您输入notify(…)时执行此操作),以及在观察者分离时将观察者添加到"最近删除"列表中。不要在这些函数周围放置互斥对象;在这些函数中的列表的创建/更新周围放置互斥对象,或者仅为此目的创建函数并在它们周围放置互斥体。

编辑:我还强烈建议创建一些单元测试用例(例如CPP单元),从多个线程中锤击附加/分离/多重分离场景。当我在做这件事的时候,为了找到一个更微妙的问题,我不得不这么做

编辑:我特别不尝试处理由于notify(…)调用而添加新观察员的情况。也就是说,有一个最近删除的列表,但没有一个最近添加的列表。这样做是为了防止"notify->add->notify->add->ect."的发生,如果有人在构造函数中粘贴notify,就会发生这种情况。

这里概述了一般方法。

该代码可在github上获取。

我在几个示例解决方案中使用了这种方法,您可以在本网站上找到这些解决方案(其中许多解决方案的代码也可以在github上找到)。

这有帮助吗?

最新更新