Правильная очистка родителей и детей с помощью обратных вызовов (C ++) - PullRequest
5 голосов
/ 23 марта 2011

Эта проблема дизайна всплывает снова и снова, и у меня все еще нет хорошего решения для нее.Это может оказаться шаблоном дизайна;) Только кажется, что он очень специфичен для 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_; 
};

Потенциальные проблемы:

Во время уничтожения родителя, он должен быть в поиске текущих обратных вызовов от своих потомков.Особенно, если эти обратные вызовы запускаются в отдельном потоке.Если это не так, он может исчезнуть к тому времени, когда вызывается обратный вызов. Если есть блокировки как в родительском, так и в дочернем элементах (очень вероятно, в многопоточном нетривиальном приложении), порядок блокировки становится проблемой: родительский объект вызывает метод для дочернего элемента, который, в свою очередь, испытывает переход состоянияи пытается уведомить родителя: dead-lock. Добавление / удаление дочерних элементов вне конструктора может быть проблемой, если дочерний элемент пытается уведомить родителя от своего деструктора.Родитель должен удерживать блокировку, чтобы изменить карту дочерних элементов, однако дочерний объект пытается выполнить обратный вызов родительского элемента.

Я только поцарапал поверхность, но можно подумать о других потенциальных проблемах.

Я ищу несколько советов о том, как справиться с чистым уничтожением родителя перед лицом потоков, блокировки и динамическое добавление / удаление детей.Если кто-нибудь придумал элегантное решение, которое является надежным при многопоточном развертывании, пожалуйста, поделитесь.Ключевое слово здесь: надежный : легко спроектировать структуру, которая сопровождается некоторыми огромными предостережениями (child никогда не вызывает parent, родительский никогда не вызывает child, нет отдельного потока для обратных вызовов и т. Д.), Задача состоит в том, чтобы поставитькак можно меньше ограничений для программиста.

Ответы [ 3 ]

3 голосов
/ 27 марта 2011

Часто большая часть проблемы с многопоточностью заключается в неспособности правильно разделить обработку (рабочий поток, т.е. дочерний процесс) и состояние. Блокировка должна выполняться с помощью потоковых структур данных, а не самих потоков. Очереди сообщений, конечные автоматы и другие подобные инструменты предназначены для того, чтобы вы могли управлять такими данными контролируемым образом, который не зависит от процессов, используемых для их обновления. Вы можете почти всегда переделать такую ​​проблему управления жизненным циклом, чтобы она стала (поточно-безопасной) проблемой управления данными. Родитель может рассматриваться как владелец состояния, а все потоки обновляют само состояние. Подсчет ссылок для управления временем жизни объектов также является обычной парадигмой.

1 голос
/ 23 марта 2011

Установите флаг, чтобы уведомить функцию ChildDone о том, что объект удаляется, и дождаться завершения любого работающего потока клиента, прежде чем вернуться из деструктора. Это гарантирует, что объект не станет недействительным, когда поток выполняет ChildDone, и никакие дальнейшие вызовы этой функции не будут приняты после вызова деструктора. (Также см. Должны ли деструкторы быть безопасными для потоков? ).

// 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;
    }
};
1 голос
/ 23 марта 2011

Если в родительском и дочернем элементах есть блокировки (очень вероятно, в многопоточном нетривиальном приложении), возникает проблема с порядком блокировки: родительский объект вызывает метод для дочернего элемента, который, в свою очередь, вызываетпереход состояния и пытается уведомить родителя: dead-lock.

Мне не понятно, почему уведомление родителя может привести к взаимоблокировке, если только

  1. родительская блокировка неудерживается в потоке A
  2. поток A ожидает, пока дочерний элемент вернет сигнал с помощью некоторого средства
  3. дочерний элемент сигнализирует родителю в потоке B
  4. родительский элемент, когда он получаетсигнал от ребенка в (3) пытается получить его блокировку

Это много ifs.И это естественно проблематичный дизайн: один поток (A) удерживает блокировку и ждет, пока другой поток (B) что-то предпримет.

Нет волшебного решения, чтобы избежать этой проблемы - вы просто должны его избежать,Лучший ответ, вероятно, состоит в том, чтобы не передавать сигнал родителю из отдельного потока;или, чтобы различать сигналы, которые будут или не будут вызываться с уже удержанной родительской блокировкой.

Во время уничтожения родительского объекта, он должен быть в поиске текущих обратных вызовов от своих дочерних элементов.Особенно, если эти обратные вызовы запускаются в отдельном потоке.Если это не так, он может исчезнуть к тому времени, когда вызывается обратный вызов.

Хитрость в том, что у детей должен быть метод (возможно, деструктор), который гарантирует, что после его возврата, ребенок не будет делать никаких обратных вызовов.Когда родитель уничтожается, он вызывает этот метод для каждого из своих потомков.

Я знаю, что вы просили "как можно меньше ограничений", но реально, работая в многопоточной среде, вы должны иметь правила для предотвращения тупиков и гонок.

...