Подтвердить что ты не робот

Проблемы с реализацией шаблона "Наблюдатель"

Я встретил интересную проблему при реализации шаблона Observer с С++ и STL. Рассмотрим этот классический пример:

class Observer {
public:
   virtual void notify() = 0;
};

class Subject {
public:
   void addObserver( Observer* );
   void remObserver( Observer* );
private:
   void notifyAll();
};

void Subject::notifyAll() {
   for (all registered observers) { observer->notify(); }
}

Этот пример можно найти в каждой книге по шаблонам проектирования. К сожалению, системы реальной жизни более сложны, поэтому вот первая проблема: некоторые наблюдатели решают добавить других наблюдателей к Субъекту при уведомлении. Это аннулирует цикл "for" и все итераторы, которые я использую. Решение довольно простое - я делаю снимок зарегистрированного списка наблюдателей и перебираю снимок. Добавление новых наблюдателей не отменяет моментальный снимок, поэтому все выглядит нормально. Но здесь возникает другая проблема: наблюдатели решают уничтожить себя при уведомлении. Хуже того, один наблюдатель может решить уничтожить всех других наблюдателей (они контролируются из сценариев), что делает недействительными как очередь, так и моментальный снимок. Я нахожусь итерацией по выделенным указателям.

Мой вопрос: как я должен справляться с ситуациями, когда наблюдатели убивают друг друга? Есть ли готовые к использованию шаблоны? Я всегда думал, что "Observer" - самый простой шаблон дизайна в мире, но теперь кажется, что его не так просто реализовать правильно...

Спасибо, всем за ваш интерес. Давайте получим сводку решений:

[1] "Не делай этого" Извините, но это обязательно. Наблюдатели контролируются из сценариев и собираются с мусором. Я не могу контролировать сбор мусора, чтобы предотвратить их выделение,

[2] "Использовать boost:: signal" Самое многообещающее решение, но я не могу повысить эффективность проекта, такие решения должны принимать только руководитель проекта (мы пишем в Playstation );

[3] "Использовать shared__ptr" . Это предотвратит выделение наблюдателей. Некоторые подсистемы могут полагаться на очистку пула памяти, поэтому я не думаю, что могу использовать shared_ptr.

[4] "Отложить освобождение наблюдателя" Наблюдатели очереди для удаления при уведомлении, затем используйте второй цикл для их удаления. К сожалению, я не могу предотвратить освобождение, поэтому я использую трюк оберточного наблюдателя с каким-то "адаптером", сохраняя фактически список "адаптеров". На деструкторе наблюдатели отменяют свои адаптеры, затем я беру свой второй цикл, чтобы уничтожить пустые адаптеры.

p.s. это нормально, что я редактирую свой вопрос, чтобы подвести итог всему сообщению? Я ноб на StackOverflow...

4b9b3361

Ответ 1

Очень интересная тема.

Попробуйте следующее:

  • Измените remObserver, чтобы обнулить запись, а не просто удалить ее (и аннулировать итераторы списка).
  • Измените цикл notifyAll:

    для (все зарегистрированные наблюдатели) {if (observer) observer- > notify(); }

  • Добавьте еще один цикл в конец notifyAll, чтобы удалить все нулевые записи из вашего списка наблюдателей

Ответ 2

Лично я использую boost:: сигналы для реализации моих наблюдателей; Мне нужно будет проверить, но я считаю, что он обрабатывает вышеупомянутые сценарии (отредактированный: нашел его, см. "Когда могут произойти отключения" ). Это упрощает вашу реализацию и не зависит от создания пользовательского класса:

class Subject {
public:
   boost::signals::connection addObserver( const boost::function<void ()>& func )
   { return sig.connect(func); }

private:
   boost::signal<void ()> sig;

   void notifyAll() { sig(); }
};

void some_func() { /* impl */ }

int main() {
   Subject foo;
   boost::signals::connection c = foo.addObserver(boost::bind(&some_func));

   c.disconnect(); // remove yourself.
}

Ответ 3

Человек идет к доктору и говорит: "Док, когда я поднимаю руку так, это очень плохо!" Врач говорит: "Не делай этого".

Самое простое решение - работать с вашей командой и говорить им, чтобы не делать этого. Если наблюдатели "действительно нуждаются", чтобы убить себя или всех наблюдателей, затем планируйте действие, когда уведомление завершено. Или, еще лучше, измените функцию remObserver, чтобы узнать, происходит ли процесс уведомления, и просто очередь на удаление, когда все будет сделано.

Ответ 4

Здесь вариация идеи T.E.D. уже представлен.

Пока remObserver может удалять запись, а не сразу ее удалять, вы можете реализовать notifyAll как:

void Subject::notifyAll()
{
    list<Observer*>::iterator i = m_Observers.begin();
    while(i != m_Observers.end())
    {
        Observer* observer = *i;
        if(observer)
        {
            observer->notify();
            ++i;
        }
        else
        {
            i = m_Observers.erase(i);
        }
    }
}

Это позволяет избежать необходимости в втором цикле очистки. Однако это означает, что если какой-то конкретный вызов notify() вызывает удаление самого себя или наблюдателя, расположенного ранее в списке, то фактическое удаление элемента списка будет отложено до следующего notifyAll(). Но до тех пор, пока любые функции, которые работают над списком, должным образом проверяют нулевые записи, когда это необходимо, это не должно быть проблемой.

Ответ 5

Проблема заключается в владении. Вы можете использовать интеллектуальные указатели, например классы boost::shared_ptr и boost::weak_ptr, чтобы продлить время жизни ваших наблюдателей до точки "де-распределения".

Ответ 6

Для решения этой проблемы существует несколько решений:

  • Использовать boost::signal позволяет автоматически удалять соединение при уничтожении объекта. Но вы должны быть очень осторожны с безопасностью потока
  • Используйте boost::weak_ptr или tr1::weak_ptr для управления наблюдателями, а boost::shared_ptr или tr1::shared_ptr для наблюдателей их подсчет саморекламы будет помогите вам в недействительности объектов, weak_ptr сообщит вам, существует ли объект.
  • Если вы используете какой-либо цикл событий, убедитесь, что каждый наблюдатель не уничтожить себя, добавить себя или любой другой в один и тот же звонок. Просто отложите работу, что означает

    SomeObserver::notify()
    {
       main_loop.post(boost::bind(&SomeObserver::someMember,this));
    }
    

Ответ 7

Как использовать связанный список в цикле for?

Ответ 8

Если ваша программа многопоточная, вам может потребоваться некоторая блокировка.

В любом случае, из вашего описания кажется, что проблема не в concurrency (multi-thrading), а скорее в мутациях, вызванных вызовом Observer:: notify(). Если это так, вы можете решить проблему, используя вектор и обходя его через индекс, а не итератор.

for(int i = 0; i < observers.size(); ++i)
  observers[i]->notify();

Ответ 9

Как насчет того, что итератор-член называется current (инициализирован как итератор end). Тогда

void remObserver(Observer* obs)
{
    list<Observer*>::iterator i = observers.find(obs);
    if (i == current) { ++current; }
    observers.erase(i);
}

void notifyAll()
{
    current = observers.begin();
    while (current != observers.end())
    {
        // it important that current is incremented before notify is called
        Observer* obs = *current++;
        obs->notify(); 
    }
}

Ответ 10

Определите и используйте итератор с тяжелыми нагрузками над контейнером уведомлений, который является устойчивым к удалению (например, обнуление, как упоминалось ранее) и может обрабатывать добавление (например, добавление)

С другой стороны, если вы хотите обеспечить сохранение контейнера const во время уведомления, объявите notifyAll и контейнер, который будет итерирован как const.

Ответ 11

Это немного медленнее, поскольку вы копируете коллекцию, но я думаю, что это тоже проще.

class Subject {
public:
   void addObserver(Observer*);
   void remObserver(Observer*);
private:
   void notifyAll();
   std::set<Observer*> observers;
};

void Subject::addObserver(Observer* o) {
  observers.insert(o);
}

void Subject::remObserver(Observer* o) {
  observers.erase(o);
}

void Subject::notifyAll() {
  std::set<Observer*> copy(observers);
  std::set<Observer*>::iterator it = copy.begin();
  while (it != copy.end()) {
    if (observers.find(*it) != observers.end())
      (*it)->notify();
    ++it;
  }
}

Ответ 12

Вы никогда не сможете избежать удаления наблюдателей во время итерации.

Наблюдатель может даже быть удален WHILE, который вы пытаетесь вызвать его функцию notify().

Поэтому я предполагаю, что вам нужен механизм try/catch.

Замок должен обеспечить, чтобы наблюдатели не изменялись при копировании набора наблюдателей

  lock(observers)
  set<Observer> os = observers.copy();
  unlock(observers)
  for (Observer o: os) {
    try { o.notify() }
    catch (Exception e) {
      print "notification of "+o+"failed:"+e
    }
  }

Ответ 13

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

Короче, вот эскиз решения:

  • Наблюдатель - это синглтон с ключами для субъекта, который регистрирует интерес. Поскольку он является одиночным, он всегда существует.
  • Каждый субъект получен из общего базового класса. Базовый класс имеет абстрактную виртуальную функцию Notify (...), которая должна быть реализована в производных классах, и деструктор, который удаляет его из Observer (который он всегда может достичь) при его удалении.
  • Внутри самого наблюдателя, если вызывается Detach (...), когда выполняется Notify (...), любые отдельные объекты попадают в список.
  • Когда Notify (...) вызывается в Observer, он создает временную копию списка Subject. По мере того как он выполняет итерацию, он сравнивает его с недавно отделившимся. Если цель не указана, на цель вызывается Notify (...). В противном случае он пропускается.
  • Уведомлять (...) в Observer также отслеживает глубину обработки каскадных вызовов (A уведомляет B, C, D и D.Notify(...) вызывает вызов Notify (...) на E и т.д.).

Это, кажется, работает хорошо. Решение размещено в Интернете здесь вместе с исходным кодом. Это относительно новый дизайн, поэтому любая обратная связь очень ценится.

Ответ 14

Я написал полный класс наблюдателей. Я включу его после проверки.

Но мой ответ на ваш вопрос: обработать дело!

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

Если наблюдатель удаляется, его деструктор сообщает всем наблюдаемым, что он подписался на об уничтожении. Если они не находятся в цикле уведомлений, в котором находится наблюдатель, тогда это наблюдаемое удаляется из std:: list < Observer *, int → gt; для этого события, если оно находится в цикле, то его запись в списке недействительна, и команда помещается в очередь, которая будет запущена, когда счетчик уведомлений опустится до нуля. Эта команда удалит недействительную запись.

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

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