如何防止调用函数调用正在被销毁的子类实例



我只需要为一个难以找到的错误进行修复,但我对修复不满意。我们的应用程序是在Windows中运行的C++,错误是纯粹的虚拟调用崩溃。以下是一些背景:

class Observable
{
public:
    Observable();
    virtual ~Observable();
    void Attach(Observer&); // Seize lock, then add Observer to the list
    void Detach(Observer&); // Seize lock, then remove Observer from the list
    void Notify(); // Seize lock, then iterate list and call Update() on each Observer
protected:
    // List of attached observers
};
class Subject : public Observable
{
    // Just important to know there is a subclass of Observable
}
class Observer
{
public:
    Observer();
    virtual ~Observer(); // Detaches itself from the Observable
    void Update() = 0;
};
class Thing : public Observer
{
public:
    void Update(); // What it does is immaterial to this question
};

因为这是一个多线程环境,所以Attach()、Detach()和Notify()都有锁。Notify()获取锁,然后迭代观察者列表,并对每个观察者调用Update()。

(我希望这是一个足够的背景,而不必发布完整的代码体。)

当一名观察员被摧毁时,问题就出现了。在毁灭时,观察者会脱离观察者。与此同时,在另一个线程中,正在对Subject调用Notify()。我最初的想法是,我们受到保护是因为Detach()和Notify()中的锁。然而,C++首先破坏子类,然后破坏基类。这意味着,在Detach()中获得阻止Notify()函数继续运行的锁之前,纯虚拟Update()函数的实现被破坏了。Notify()函数继续(因为它已经获得了锁),并试图调用Update()。结果是崩溃。

现在,这是我的解决方案,它有效,但给我一种恶心的感觉。我将Update()函数从纯虚拟更改为仅虚拟,并提供了一个什么都不做的主体。这让我感到困扰的原因是,Update()仍在被调用,但它是在一个部分销毁的对象上。换言之,我做了一些事情,但我对实现并不狂热。

讨论的其他选项:

1) 将锁定移动到子类中。这是不可取的,因为它迫使每个子类的开发人员复制逻辑。如果他省略了锁定,坏事就会发生。

2) 通过Destroy()函数强制销毁观察者。老实说,我不知道如何为基于堆栈的对象实现这一点。

3) 让子类在其析构函数中调用一个"PreDestroy()"函数,通知基类即将发生破坏。我不知道如何强制执行,忘记它可能会导致很难找到运行时错误。

有人能为防止此类撞车事故的发生提供更好的方法吗?我有一种不愉快的感觉,我想念房间里的大象。

JAB

这个问题说明了多线程设计的一个更普遍的结果:任何受多个线程影响的对象都不能保证在任何时间点都不会对自己进行并发访问。这个结果就是房间里的大象给了你在问题结束时描述的不愉快的感觉。

简而言之,您的问题是Attach()Detach()Notify()负责获取适当的锁并执行它们的任务。他们需要能够在接到电话之前假设锁被抓住了。

不幸的是,该解决方案需要更复杂的设计。对于独立于对象(或类)的单个源,有必要对所有对象的构建、更新(包括附加和分离)和销毁进行中介。有必要防止这些过程中的任何一个独立于调解人而发生。

无论是通过技术手段(如访问控制等)阻止这些过程,还是简单地声明所有类型都必须遵守的策略,这都是一种设计选择。这个选择取决于你是否可以依靠你的开发人员(包括你自己)来遵循政策指导方针。

关于您的解决方案:

  1. 不好,因为你给出的理由
  2. 好的。此外,将~Observer()定义为protected(作为派生类的任何其他析构函数),以避免直接调用delete并编写成员void Destroy()。问题是它不适用于自动(局部)变量。实际上,使用受保护的析构函数,您将无法声明局部变量
  3. 您的意思可能是从每个子类析构函数的析构函数调用PreDestroy()。问题是不要忘记它(你可以断言它是从~Observer()调用的),如果你有几个级别的继承

关于您的原始解决方案:

  1. 使Update()成为一个可调用的虚拟函数似乎是可行的,但在技术上是错误的。当一个线程使用vtable指针调用虚拟Update()时,另一个线程调用~Thing()并将vtable指针更新为Observer()的指针。第一个线程持有锁,但第二个线程没有,所以你有一场比赛

我的建议是使用选项2,除非您非常喜欢Observer的子类的自动实例。


如果你愿意,你可以尝试使用模板:

template<typename O>
class ObserverPtr : Observer
{
 public:
    ObserverPtr(O *obj)
     :m_obj(obj)
    {}
    void Update()
    {
        m_obj->Update();
    }
    ~ObserverPtr()
    {
        PreDestroy();
        delete m_obj;
    }
 private:
    O *m_obj;
};

则CCD_ 15将不会从CCD_。

您也可以创建此模板的替代变体:

  • 以保持对真实观察者的引用而不是指针(O &m_obj;)。不使用delete
  • 以将真实观察者定义为实际成员(O m_obj;)。没有动态分配
  • 将智能指针指向真实的观察者

我认为,除非您强制从派生类析构函数中调用Detach,否则它将不会有一个简单的解决方案。

我想到的一个解决方案可能是使用一个额外的"包装器"来处理取消注册(实际上也可能进行注册)。

类似这样的东西(例如):

class ObserverAgent;
// the wrapper class - not virtual
class Observer
{
public:
    Observer(Observable &subject, ObserverAgent &agent)
        : _subject(subject), _agent(agent)
    {
        _subject.Attach(*this);
    }
    ~Observer()
    {
        _subject.Detach(*this);
    }
    void Update()
    {
        _agent.Update();
    }
private:
    Observable &_subject;
    ObserverAgent &_agent;
};
// the actual observer polymorphic object
class ObserverAgent
{
public:
    ObserverAgent();
    virtual ~ObserverAgent();
protected:
    // only callable by the Observer wrapper
    friend class Observer;
    virtual void Update() = 0;
};
class Thing : public ObserverAgent
{
public:
    virtual void Update();
};

这样的使用就更加复杂了:

Subject s;
{ // some scope
    Thing t;
    Observer o(s, t);
    // do stuff
} // here first Observer is detached and destroyed, then Thing (already unregistered)

请注意,您不能直接附加/分离Thing(Observer Agent),只能通过Observer(因为Observable接受Observer,而不是Observer代理)。

也许可以将其封装在一个类中以更简单地使用(Agent是Observer的内部类),但是,代理的寿命可能会出现问题(因为Observer的销毁必须再次是虚拟的)。

为什么不在Observable中将锁公开为protected?如果这个线程还没有获取锁,Observable::~Observable就会获取锁,然后继续清理。同时,Observable的每个子类都在其dtor中获取锁,而不需要进一步释放它(这只在~Observable本身中完成)。

坦率地说,在这个设计中,最简单、最一致的解决方案似乎是在最派生类的析构函数的一开始手动分离()每个Observer。我看不出这是如何自动化的,既然必须在毁灭的黎明做些什么,为什么不超然呢。

好吧,如果我们不需要更深入的话:

template<class Substance> struct Observer {
    Observer(Observable &o): o(o) { o.attach(s); }
    ~Observer() { o.detach(s); }
    Substance &subst() const { return s; }
private:
    Observable &o;
    Substance s;
};
struct ThingSubst { void update(); long stats(); };
typedef Observer<ThingSubst> Thing;
Observable o;
Thing thing(o);
std::cout << thing.subst().stats() << std::endl;

最新更新