32

在使用 C++ 和 STL 实现观察者模式时,我遇到了一个有趣的问题。考虑这个经典的例子:

class Observer {
public:
   virtual void notify() = 0;
};

class Subject {
public:
   void addObserver( Observer* );
   void remObserver( Observer* );
private:
   void notifyAll();
};

void Subject::notifyAll() {
   for (all registered observers) { observer->notify(); }
}

这个例子可以在每一本关于设计模式的书中找到。不幸的是,现实生活中的系统更复杂,所以这是第一个问题:一些观察者决定在收到通知时将其他观察者添加到主题中。这使我使用的“for”循环和所有迭代器无效。解决方案相当简单——我对已注册的观察者列表进行快照并遍历该快照。添加新的观察者不会使快照失效,所以一切看起来都很好。但是这里还有另一个问题:观察者决定在收到通知时摧毁自己。更糟糕的是,一个观察者可以决定销毁所有其他观察者(它们由脚本控制),这会使队列和快照都无效。我发现自己迭代了解除分配的指针。

我的问题是,当观察者互相残杀时,我应该如何处理这种情况?有没有现成的模式?我一直认为“观察者”是世界上最简单的设计模式,但现在看来要正确实现它并不是那么容易......

谢谢大家的关注。让我们有一个决策摘要:

[1]“不要这样做”对不起,但这是必须的。观察者由脚本控制并被垃圾收集。我无法控制垃圾收集以防止它们被取消分配;

[2] “使用 boost::signal”最有希望的决定,但我不能在项目上引入 boost,这样的决定只能由项目负责人做出(我们在 Playstation 下编写);

[3] “使用 shared__ptr”这将阻止观察者解除分配。一些子系统可能依赖于内存池清理,所以我认为我不能使用 shared_ptr。

[4] “推迟观察者释放”在通知的同时将观察者排队等待移除,然后使用第二个周期将它们删除。不幸的是,我无法阻止释放,所以我使用了一种用某种“适配器”包装观察者的技巧,实际上保留了“适配器”列表。在析构函数上,观察者从他们的适配器中取消分配,然后我用我的第二个周期来销毁空的适配器。

ps 可以吗,我编辑我的问题以总结所有帖子?我是 StackOverflow 上的菜鸟...

4

14 回答 14

15

非常有趣的问题。

试试这个:

  1. 将 remObserver 更改为使条目无效,而不是仅仅删除它(并使列表迭代器无效)。
  2. 将您的 notifyAll 循环更改为:

    for (所有注册的观察者) { if (observer) observer->notify(); }

  3. 在 notifyAll 末尾添加另一个循环以从观察者列表中删除所有空条目

于 2009-06-19T14:39:12.307 回答
7

就个人而言,我使用boost::signals来实现我的观察者;我必须检查一下,但我相信它可以处理上述情况(已编辑:找到它,请参阅“何时会发生断开连接”)。它简化了您的实现,并且不依赖于创建自定义类:

class Subject {
public:
   boost::signals::connection addObserver( const boost::function<void ()>& func )
   { return sig.connect(func); }

private:
   boost::signal<void ()> sig;

   void notifyAll() { sig(); }
};

void some_func() { /* impl */ }

int main() {
   Subject foo;
   boost::signals::connection c = foo.addObserver(boost::bind(&some_func));

   c.disconnect(); // remove yourself.
}
于 2009-06-19T14:29:01.810 回答
6

一个男人去找医生说:“医生,当我这样举起手臂时,真的很痛!” 医生说:“不要那样做。”

最简单的解决方案是与您的团队合作并告诉他们不要这样做。如果观察者“真的需要”杀死自己或所有观察者,则安排通知结束时的操作。或者,更好的是,更改 remObserver 函数以了解是否正在发生通知过程,并在一切完成后将删除排队。

于 2009-06-19T14:30:56.113 回答
5

这是TED已经提出的想法的变体。

只要 remObserver 可以使条目为空而不是立即删除它,那么您可以将 notifyAll 实现为:

void Subject::notifyAll()
{
    list<Observer*>::iterator i = m_Observers.begin();
    while(i != m_Observers.end())
    {
        Observer* observer = *i;
        if(observer)
        {
            observer->notify();
            ++i;
        }
        else
        {
            i = m_Observers.erase(i);
        }
    }
}

这避免了对第二个清理循环的需要。但是,这确实意味着,如果某个特定的 notify() 调用触发了删除其自身或位于列表中较早位置的观察者,则列表元素的实际删除将推迟到下一个 notifyAll()。但是只要在列表上运行的任何函数在适当的时候适当注意检查空条目,那么这应该不是问题。

于 2009-06-20T18:40:15.783 回答
4

问题是所有权问题。您可以使用智能指针(例如boost::shared_ptrandboost::weak_ptr类)来延长观察者的生命周期,使其超过“解除分配”点。

于 2009-06-19T14:31:23.537 回答
3

这个问题有几种解决方案:

  1. 使用boost::signal它允许在对象销毁时自动删除连接。但是你应该非常小心线程安全
  2. 使用boost::weak_ptrortr1::weak_ptr来管理观察者,boost::shared_ptr或者tr1::shared_ptr为观察者自己管理 - 引用计数将帮助您使对象无效,weak_ptr 将让您知道对象是否存在。
  3. 如果您在某个事件循环上运行,请确保每个观察者不会在同一个调用中破坏自己、添加自己或任何其他对象。只是推迟工作,意思是

    SomeObserver::notify()
    {
       main_loop.post(boost::bind(&SomeObserver::someMember,this));
    }
    
于 2009-06-19T14:36:40.143 回答
0

for在循环中使用链表怎么样?

于 2009-06-19T14:22:08.207 回答
0

如果您的程序是多线程的,您可能需要在此处使用一些锁定。

无论如何,根据您的描述,问题似乎不是并发(多线程),而是由 Observer::notify() 调用引起的突变。如果是这种情况,那么您可以通过使用向量并通过索引而不是迭代器遍历它来解决问题。

for(int i = 0; i < observers.size(); ++i)
  observers[i]->notify();
于 2009-06-19T14:28:32.763 回答
0

如何调用一个成员迭代器current(初始化为end迭代器)。然后

void remObserver(Observer* obs)
{
    list<Observer*>::iterator i = observers.find(obs);
    if (i == current) { ++current; }
    observers.erase(i);
}

void notifyAll()
{
    current = observers.begin();
    while (current != observers.end())
    {
        // it's important that current is incremented before notify is called
        Observer* obs = *current++;
        obs->notify(); 
    }
}
于 2009-06-19T16:22:55.330 回答
0

在通知器容器上定义和使用重载迭代器,该迭代器对删除有弹性(例如,如前所述,清空)并且可以处理添加(例如附加)

另一方面,如果您想在通知期间强制保持容器 const,请将 notifyAll 和被迭代的容器声明为 const。

于 2009-06-19T18:54:30.360 回答
0

由于您正在复制集合,所以这有点慢,但我认为它也更简单。

class Subject {
public:
   void addObserver(Observer*);
   void remObserver(Observer*);
private:
   void notifyAll();
   std::set<Observer*> observers;
};

void Subject::addObserver(Observer* o) {
  observers.insert(o);
}

void Subject::remObserver(Observer* o) {
  observers.erase(o);
}

void Subject::notifyAll() {
  std::set<Observer*> copy(observers);
  std::set<Observer*>::iterator it = copy.begin();
  while (it != copy.end()) {
    if (observers.find(*it) != observers.end())
      (*it)->notify();
    ++it;
  }
}
于 2010-09-13T06:34:17.187 回答
0

您永远无法避免在迭代时移除观察者。

观察者甚至可以在WHILE你试图调用它的notify()函数时被移除。

因此我想你需要一个try/catch机制。

锁是为了确保在复制观察者集时观察者集不会改变

  lock(observers)
  set<Observer> os = observers.copy();
  unlock(observers)
  for (Observer o: os) {
    try { o.notify() }
    catch (Exception e) {
      print "notification of "+o+"failed:"+e
    }
  }
于 2011-01-20T14:03:25.313 回答
0

几个月前,当我看到这篇文章时,我正在寻找解决这个问题的方法。它让我开始思考解决方案,我认为我有一个不依赖于 boost、智能指针等的解决方案。

简而言之,这是解决方案的草图:

  1. Observer 是一个单例,带有用于注册感兴趣的主题的键。因为它是一个单例,所以它始终存在。
  2. 每个主题都派生自一个公共基类。基类有一个抽象虚函数 Notify(...) 必须在派生类中实现,以及一个析构函数,当它被删除时,它会从 Observer 中删除(它总是可以到达)。
  3. 在观察者本身内部,如果在 Notify(...) 正在进行时调用 Detach(...),则任何分离的 Subjects 最终都会出现在列表中。
  4. 当在 Observer 上调用 Notify(...) 时,它会创建主题列表的临时副本。当它迭代它时,它将它与最近分离的进行比较。如果目标不在其上,则在目标上调用 Notify(...)。否则,将被跳过。
  5. Observer 中的 Notify(...) 还跟踪处理级联调用的深度(A 通知 B、C、D,并且 D.Notify(...) 触发对 E 的 Notify(...) 调用, ETC。)

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

于 2013-10-06T01:04:24.147 回答
0

我刚刚写了一个完整的观察者类。我会在测试后将其包含在内。

但我对你的问题的回答是:办案!

我的版本确实允许在通知循环内触发通知循环(它们立即运行,将其视为深度优先递归),但是有一个计数器,以便 Observable 类知道通知正在运行以及有多少深度。

如果观察者被删除,它的析构函数会告诉所有它订阅的可观察对象关于销毁的事情。如果它们不在观察者所在的通知循环中,则该可观察对象将从该事件的 std::list<pair<Observer*, int>> 中删除,如果它处于循环中,则它在list 无效,并且命令被推入队列,当通知计数器降至零时将运行该队列。该命令将删除无效的条目。

所以基本上,如果你不能安全地删除(因为可能有一个迭代器持有将通知你的条目),那么你使条目无效而不是删除它。

因此,与所有并发无等待系统一样,规则是 - 如果您没有被锁定,则处理此案,但如果您被锁定,那么您将工作排队,持有锁的人将在他释放锁时完成工作。

于 2014-08-18T07:30:01.367 回答