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

时间:2016-04-07 18:46:40

标签: c++ multithreading

我只需要修复一个难以找到的错误,但我对修复程序感到不满意。我们的应用程序是在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()。

(我希望这已经足够了,无需发布完整的代码体。)

当观察者被摧毁时出现问题。在破坏时,Observer将自己从Observable中分离出来。同时,在另一个线程中,在Subject上调用Notify()。我最初的想法是,我们受到保护,因为Detach()中的锁定(调用Observer的破坏)和Notify()。但是,C ++首先破坏子类,然后是基类。这意味着,在获得Detach()的锁定之前,这将阻止Notify()函数继续执行,纯虚拟Update()函数的实现被破坏。 Notify()函数继续(因为它已经获得了锁)并尝试调用Update()。结果是崩溃。

现在,这是我的解决方案,它起作用,但给我一种不安的感觉。我将Update()函数从纯虚拟更改为虚拟虚拟,并提供了一个不执行任何操作的主体。这让我困扰的原因是Update()仍然被调用,但是在一个部分被破坏的对象上。换句话说,我正在逃避一些事情,但我对实施并不满意。

讨论了其他选项:

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

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

3)让子类调用" PreDestroy()"函数在析构函数中通知基类即将发生破坏。我不知道如何强迫这一点,忘记它可能导致难以发现的运行时错误。

任何人都可以提供任何建议,以更好的方式来防止这些类型的崩溃?我有这种不愉快的感觉,我在房间里想念大象。

JAB

4 个答案:

答案 0 :(得分:1)

关于您的解决方案:

  1. 不好,因为你给出的理由。
  2. 确定。另外,将~Observer()定义为protected(与派生类的任何其他析构函数一样),以避免直接调用delete并编写成员void Destroy()。问题是它不适用于自动(本地)变量。实际上,使用受保护的析构函数,您将无法声明局部变量。
  3. 您可能意味着从每个子类析构函数的析构函数中调用PreDestroy()。问题不是忘记它(你可以断言它已经从~Observer()调用)并且你有几个级别的继承。
  4. 关于您的原始解决方案:

    1. 使Update()可调用的虚函数似乎有效,但从技术上讲是错误的。当一个线程使用vtable指针调用虚拟Update()时,另一个线程正在调用~Thing()并将vtable指针更新为Observer()的指针。第一个线程持有锁,但第二个线程没有,所以你有一个种族。
    2. 我的建议是使用选项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;
      };
      

      然后Thing不会来自Observer

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

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

答案 1 :(得分:1)

此问题说明了多线程设计的更一般结果:没有受多线程影响的对象可以保证在任何时间点都无法并发访问自身。这个后果就是房间里的大象会给你在问题结尾处描述的不愉快的感觉。

简而言之,您的问题是Attach()Detach()Notify()负责抓取适当的锁并执行其操作。他们需要能够在被叫之前抓住锁。

不幸的是,该解决方案需要更复杂的设计。某些独立于您的对象(或类)的单个源必须调整构造,更新(包括附加和分离)以及销毁所有对象。并且有必要防止任何这些过程独立于调解员发生。

这是一种设计选择,您是通过技术手段(例如访问控制等)阻止这些流程,还是简单地说明所有类型必须遵守的政策。这种选择取决于您是否可以依赖您的开发人员(包括您自己)来遵循政策指导原则。

答案 2 :(得分:0)

我认为除非你要求从派生类析构函数中调用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)

请注意,您不能直接通过Observer附加/分离Thing(ObserverAgent)(因为Observable采用Observer,而不是ObserverAgent)。

也许可以将它包装在单个类中以便更简单地使用(代理是Observer的内部类),但是代理的生命周期可能存在问题(因为Observer的破坏会有再次虚拟。)

答案 3 :(得分:0)

为什么不在protected中将锁定为ObservableObservable::~Observable获取锁定,如果该线程尚未获取锁定,则继续进行清理。同时,Observable的每个子类都在其dtor中获取锁定而不进一步释放它(仅在~Observable本身中完成)。

坦率地说,在这个设计中,最简单和最一致的解决方案似乎是在最派生类的析构函数的最开始手动Detach()每个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;