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

时间:2011-03-23 03:11:51

标签: c++ multithreading locking deadlock

这个设计问题一再出现,我仍然没有一个很好的解决方案。它可能会变成一种设计模式;)但是,它似乎非常特定于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_; 
};

潜在问题:   

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

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

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

  • 4 个答案:

    答案 0 :(得分:3)

    多线程问题的很大一部分通常是无法正确分离处理(工作线程,即Child)和状态。 锁定应该通过线程安全的数据结构而不是线程本身来完成。 消息队列,状态机和其他此类工具旨在允许您以受控方式管理此类数据,该方式独立于用于更新它们的过程。 您几乎总能重做这样的生命周期管理问题,以便它成为(线程安全的)数据管理问题。父可以被认为是状态的所有者,并且所有线程都更新状态本身。 用于管理对象生命周期的引用计数也是一种常见的范例。

    答案 1 :(得分:1)

      

    如果父母都有锁   和孩子们(非常可能是一个孩子   多线程非平凡   应用程序),锁定顺序成为一个   问题:父调用方法   孩子,反过来经历一个   状态转换并尝试通知   父母:死锁。

    我不清楚为什么通知父母会导致死锁,除非

    1. 父锁定在线程A
    2. 线程A正在等待孩子通过某种方式返回信号
    3. 孩子在线程B中发信号通知父母
    4. 父母在收到来自孩子的信号时(3)试图获得其锁定
    5. 那是很多ifs。这是一个自然有问题的设计:一个线程(A)持有一个锁,并等待另一个线程(B)做某事。

      没有神奇的解决方案来避免这个问题 - 你只需要避免它。最好的答案可能是不从单独的线程发回父母的信号;或者,用于区分已经或将不会被已经持有的父锁调用的信号。

        

      在破坏父母的过程中,它   必须注意持续进行   来自其子女的回调。   特别是如果那些回调是   在一个单独的线程中被解雇了。如果它   不是,它可能已经消失了   调用回调。

      这里的诀窍可能是孩子们应该有一个方法(也许是析构函数),这个方法在它返回后,孩子将不再进行回调。当父节点被销毁时,它会为每个子节点调用该方法。

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

    答案 2 :(得分:1)

    设置一个标志以通知ChildDone函数正在删除该对象,并在从析构函数返回之前等待任何正在运行的客户端线程完成。这可确保在线程执行ChildDone时对象不会变为无效,并且在调用析构函数后不再接受对该函数的进一步调用。 (另见Should destructors be threadsafe?)。

    // Pseudocode, not compilable C++.
    class Parent {
    
         // ....
    
        ~Parent() {
            mutex_.acquire();
            shuttingDown_ = true;
            mutex_.release();
    
            foreach (Child child in children_)
                child->detachParent();
           waitForRunningClientThreadToExit();
        }
    
        void ChildDone(const string& child) {
          mutex_.acquire(); 
          if (!shuttingDown_)
              cout << "Child is DONE: " << child << endl;
          mutex_.release();
        }
    
        bool volatile shuttingDown_ = false;
        Mutex mutex_;
    
        // ....
    
    };
    
    class Child {
    
        // ...
    
        Foo(int guess) {
           if (guess == 42) done_ = true;
           if (parent)
               parent->ChildDone(name_);
        } 
    
        void detachParent() {
            parent = NULL;
        }
    };
    

    答案 3 :(得分:0)

    使用shared_ptrenable_shared_from_thisweak_ptr三重奏有一个解决方案。查看修改后的代码:

    class Parent : public std::enable_shared_from_this<Parent> {
    
        ~Parent()
        {
            shuttingDown = true;
        }
    
        void addChild()
        {
            Child c{ "some_name", weak_from_this() };
            c.foo();
        }
    
        void childDone(const std::string& child)
        {
            if (!shuttingDown)
                std::cout << "Child is DONE: " << child << std::endl;
        }
    
        std::atomic_bool shuttingDown = false;
    
        struct Child {
            std::string name;
            std::weak_ptr<Parent> parent;
    
            void foo()
            {
                //do smth
                if (auto shared_parent = parent.lock()) {
                    shared_parent->childDone(name);
                }
            }
        };
    };
    

    此代码有效,但有一个严重的缺点-父对象无法在堆栈上分配,必须始终通过创建shared_ptr<Parent>来创建它。 Here's a link to another question关于如何摆脱该限制。