通过回调正确清理父母和孩子(C++)

Laj*_*agy 5 c++ multithreading deadlock locking

这个设计问题一次又一次地出现,我仍然没有一个很好的解决方案.它可能会变成一种设计模式;)但是,它似乎非常特定于C++(缺少垃圾收集).无论如何,这是问题所在:

我们有一个父对象,它保持对子对象的引用.父母的州取决于其子女的州(某些总和).为了通知其子女的状态变化,它会向他们传递对自己的引用.(在另一个变体中,它向它们传递一个回调,子进程可以调用它来通知父进程.这个回调是一个闭包,它保持对父进程的引用.)应用程序是多线程的.现在,这个设置是整个大黄蜂的潜在竞争条件和死锁的巢.要理解原因,这是一个天真的实现:

class Parent {
 public:
   Parent() {
     children_["apple"].reset(new Child("apple", this));
     children_["peach"].reset(new Child("peach", this));
   }

   ~Parent() {
   }

   void ChildDone(const string& child) {
     cout << "Child is DONE: " << child << endl;
   }

  private:
   map<string, linked_ptr<Child> > children;
}; 

class Child {
  public:
   Child(const string& name, Parent* parent) 
       : name_(name), parent_(parent), done_(false) {}

   Foo(int guess) {
     if (guess == 42) done_ = true;
     parent->ChildDone(name_);
   }

  private:
   const string name_;
   Parent* parent_;
   bool done_; 
};
Run Code Online (Sandbox Code Playgroud)

潜在问题:

  • 在破坏父母期间,必须留意其子女正在进行的回调.特别是如果那些回调在一个单独的线程中被触发.如果不是,则在调用回调时可能会消失.
  • 如果父级和子级中都存在锁(很可能是在多线程的非平凡应用程序中),则锁定顺序成为问题:父级调用子级上的方法,而子级又经历状态转换并尝试通知父母:死锁.
  • 如果子项尝试从其析构函数通知父项,则在构造函数外添加/删除子项可能会出现问题.父级必须持有锁以修改子级映射,但子级正在尝试对父级进行回调.

    我只是触及了表面,但人们可以想到其他潜在的问题.

    我正在寻找的是关于如何在线程,锁定和动态添加/删除子项时处理父类的干净破坏的一些建议.如果有人提出了一个在多线程部署下强大的优雅解决方案,请分享.这里的关键字是健壮的:很容易设计一个带有一些巨大警告的结构(子节点从不调用父节点,父节点从不调用子节点,没有单独的线程用于回调等),挑战在于对程序员的限制很少尽可能.

  • dav*_*mac 1

    如果父级和子级中都有锁(很可能在多线程的非平凡应用程序中),则锁定顺序将成为一个问题:父级调用子级上的方法,而子级又会经历状态转换并尝试通知父级:死锁。

    我不清楚为什么通知家长会导致僵局,除非

    1. 父锁被线程 A 持有
    2. 线程 A 正在等待子进程通过某种方式返回信号
    3. 子进程在线程 B 中向父进程发送信号
    4. 父进程在 (3) 中收到来自子进程的信号时尝试获取其锁

    这是很多如果。这是一种自然有问题的设计:一个线程 (A) 持有锁,并等待另一个线程 (B) 执行某些操作。

    没有什么神奇的解决方案可以避免这个问题——你只需避免它。最好的答案可能是不要从单独的线程向父级发回信号;或者,区分在已持有父锁的情况下将调用或不会调用的信号。

    在销毁父级期间,它必须留意来自其子级的持续回调。特别是如果这些回调是在单独的线程中触发的。如果不是,则在调用回调时它可能已经消失。

    这里的技巧可能是子级应该有一个方法(可能是析构函数),该方法保证在返回后,子级将不再进行回调。当父对象被销毁时,它会为其每个子对象调用该方法。

    我知道您要求“尽可能少的限制”,但实际上,在多线程环境中工作时,您必须制定规则来防止死锁和竞争。