使用weak_ptr的观察者模式

Observer pattern using weak_ptr

本文关键字:观察者模式 ptr weak 使用      更新时间:2023-10-16

我试图从观察者模式编写一个安全的Subject类。我想知道使用weak_ptr是否是以以下方式存储IObserver实例的最佳方式:

  • IObserver实例被释放后不能再使用。
  • Subject类没有保持应该被释放的IObserver引用(失效侦听器问题)。
  • Subject类必须是线程安全的。

不幸的是,我们的编码标准规定不允许使用boost。我想我前世是个坏人。幸运的是,我被允许使用c++ 11 (Visual Studio 2012附带的)。

下面是一个示例Observer类。

// Observer interface that supports notify() method
class IObserver
{
public:
    virtual void notify() const = 0;
    virtual ~IObserver() {}
};
// Concrete observer implementation that prints a message
class Observer : public IObserver
{
public:
    Observer( const std::string& message) : m_message( message ){}
    void notify() const {
        printf( "%srn", m_message.c_str() );
    }
private:
    std::string m_message;
};

这是Subject类。

// Subject which registers observers and notifies them as needed.
class Subject
{
public:
    // Use shared_ptr to guarantee the observer is valid right now
    void registerObserver( const std::shared_ptr<IObserver>& o )
    {
        std::lock_guard<std::mutex> guard( m_observersMutex );
        m_observers.push_back( o );
    }
    void unregisterObserver( const std::shared_ptr<IObserver>& o )
    {
        std::lock_guard<std::mutex> guard( m_observersMutex );
        // Code to remove the observer from m_observersMutex
    }
    // This is a method that is run in its own thread that notifies observers of some event
    void doNotify()
    {
        std::lock_guard<std::mutex> guard( m_observersMutex );
        // Notify any valid observers of events.
        std::for_each( m_observers.cbegin(), m_observers.cend(), 
            []( const std::weak_ptr<IObserver>& o )
        {
            auto observer = o.lock();
            if ( observer ) {
                observer->notify();
            } 
        } );
        // Remove any dead observers.  These are ones which have expired().
        m_observers.erase( std::remove_if( m_observers.begin(), m_observers.end(), 
            []( const std::weak_ptr<IObserver>& o )
        {
            return o.expired();
        } ), m_observers.end() );
    }

private:
    std::vector<std::weak_ptr<IObserver>> m_observers;
    std::mutex m_observersMutex;
};
下面是一些执行Subject的代码:
int main(int argc, wchar_t* argv[])
{
    Subject subject;
    auto observerHello = std::make_shared<Observer>( "Hello world" );
    subject.registerObserver( observerHello );
    {
        // Create a scope to show unregistration.
        auto observerBye = std::make_shared<Observer>( "Good bye" );
        subject.registerObserver( observerBye );
        subject.doNotify();
    }
    printf( "%srn", "Observer good bye is now be destructed" );
    subject.doNotify();
    return 0;
}

我使用weak_ptr线程安全吗?从这里https://stackoverflow.com/a/2160422/1517648我认为是。

这是解决失效侦听器问题的合法方法吗?

我会有点怀疑你的doNotify -假设在一个观察者你火最终添加或删除观察者?——不好的事情会发生(包括崩溃)。或者阻塞了另一个线程的动作,谁阻塞了试图添加观察者?——不好的事情发生了(死锁!)

这个问题很难解决。基本上,这是一个可重入性的问题。

当你持有锁时,永远不要放弃对代码的控制。在调用回调时持有锁是不允许的。

所以,至少:

锁定,然后复制您的列表,然后解锁。在复制的同时,你也可以删除过期的观察者(从原始列表和复制列表中)。

然后从复制的列表中释放观察者。

这留下了一些未解决的问题。例如,删除一个观察者并不能保证它将来不会被调用!它只是意味着最终它不会被调用。

这有多重要取决于你如何使用倾听。

一种可能有效的方法是一个包含add/remove/notify/killthread事件的任务队列(将killthread设置为队列中的任务可以使关闭变得不那么烦人)。现在所有同步都在队列中。如果不打算编写非阻塞无锁队列,notify代码可以简单地锁定、std::move队列、解锁,然后继续执行它。或者你可以写一个队列,pop阻塞,直到有东西可读,而push不阻塞。

一个快速而肮脏的"复制和广播"可能是这样的:

std::vector<std::shared_ptr<IObserver>> targets;
{
  std::lock_guard<std::mutex> guard( m_observersMutex );
  m_observers.erase( std::remove_if( m_observers.begin(), m_observers.end(), 
        [&targets]( const std::weak_ptr<IObserver>& o )
    {
      std::shared_ptr<IObserver> ptr = o.lock();
      if (ptr) {
        targets.push_back(ptr);
        return false;
      } else {
        return true;
      }
    } ), m_observers.end() );
}
for( auto& target:targets ) {
  target->notify();
}