2009-06-19 5 views
31

Я столкнулся с интересной проблемой при реализации шаблона Observer с C++ и 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» и все итераторы, которые я использую. Решение довольно простое - я делаю снимок зарегистрированного списка наблюдателей и перебираю снимок. Добавление новых наблюдателей не отменяет моментальный снимок, поэтому все выглядит нормально. Но здесь возникает другая проблема: наблюдатели решают уничтожить себя при уведомлении. Хуже того, один наблюдатель может решить уничтожить всех других наблюдателей (они контролируются из сценариев), что делает недействительными как очередь, так и моментальный снимок. Я нахожусь итерацией по выделенным указателям.

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

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

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

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

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

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

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

+1

Хороший вопрос! Я не рассматривал использование шаблона наблюдателя, где наблюдателям разрешено создавать и уничтожать других наблюдателей субъекта. –

+0

Мне нравится суммировать ответы на вопрос в вопросе, просто не изменяйте исходный вопрос с удалением, или позже читатели могут пропустить нюансы исходного вопроса (не то, что вы сделали, я думаю, что ваше подведение итогов и замечание о том, что его обобщенный). – Jay

+0

Вы когда-нибудь испытывали какие-либо из них, чтобы увидеть, какой из них вам понравился, лучше всего или лучше всего работал? – prolink007

ответ

14

Очень интересный вопрос.

Попробуйте это:

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

    для (всех зарегистрированных наблюдателей) {если (наблюдатель) наблюдатель> Notify(); }

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

+0

Этот звук наиболее подходит для меня, поскольку я не могу использовать сигналы или shared_ptr. Хотя два цикла вместо одного могут привести к снижению производительности, я думаю, что это самый простой способ. Благодаря! – SadSido

+1

Я полагаю, что если вы действительно обеспокоены performace, вы можете добавить какой-то «грязный» флаг, чтобы последний цикл не активировался, если нет чего-то, что нужно удалить. Тем не менее, я бы не стал беспокоиться, если в этом цикле не была проверена и измерена производительность. Преждевременная оптимизация и все такое. –

0

Как насчет использования связанного списка в вашей петле for?

+0

Я использую std :: list прямо сейчас. Но это не устраняет проблему: операции удаления по-прежнему недействительны итерации по списку. Должен ли я писать что-то более сложное внутри цикла «for»? – SadSido

+0

Либо это (проверьте «следующий» указатель на NULL и метод удаления, убедитесь, что он NULLED) или используйте boost :: signal, как предложил Тодд Гарднер. –

0

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

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

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

Использование вектора не является решением, так как оно оставляет некоторые из наблюдателей необработанными ... У вас есть i = 3, наблюдатель # 3 убивает себя, вектор сдвигается, вы увеличиваете i и ... один из наблюдателей остается незамеченным :) – SadSido

+0

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

7

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

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. 
} 
+0

+1 для boost :: signal Точно так же я реализовал наблюдателей, и это делает жизнь намного проще –

6

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

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

+0

Это метод, который я использовал с измененным шаблоном наблюдателя. Если удаление HAS должно выполняться немедленно, если вы застряли, что усложняет вашу логику обработки угловых случаев. –

+1

По общему признанию, это был мой первый импульс. Однако, я думаю, что это отношение точно показывает, что не так с «образцами». Разработка программного обеспечения заключается не в том, чтобы объединить идеальные кирпичи. Речь идет о решении проблем. –

+2

@ T.E.D. Я не мог согласиться с тем, что шаблон не является одним из Священных Писаний, которые никогда не должны быть омрачены изменениями. Но я часто обнаружил, что разработчики слишком сильно наклоняются в сторону усложнения вещей с быстрыми исправлениями и часто не делают шаг назад и спрашивают, какова настоящая проблема. Может быть, они используют шаблон Observer в ситуации, для которой он не был разработан. – Lee

4

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

3

Есть несколько решений этой проблемы:

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

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

Как о том, член итератора под названием 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's important that current is incremented before notify is called 
     Observer* obs = *current++; 
     obs->notify(); 
    } 
} 
+0

Эта стратегия может быть легко побеждена, когда наблюдатель решит повторно вызвать уведомление All on Subject. Сколько же «нынешних» членов должны подчиняться? Ну, я думаю, это теоретическая проблема - мы должны действительно ограничивать наших наблюдателей. Спасибо за ответ! – SadSido

0

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

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

+0

Пользовательский итератор, который не может быть признан недействительным, также является хорошим решением. Это займет некоторое время, но это будет очень полезно. Благодаря! – SadSido

5

Вот вариант на тему 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(). Но до тех пор, пока любые функции, которые работают над списком, должным образом проверяют нулевые записи, когда это необходимо, тогда это не должно быть проблемой.

+0

Да, это должно быть сделано. –

0

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

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; 
    } 
}
+0

Проблема, которую я вижу, проверяет «find (* it)» для каждого элемента, а не на копирование коллекции. Это увеличивает сложность и может быть болезненным, когда у вас много наблюдателей. Во всяком случае, идея классная, спасибо! – SadSido

0

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

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

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

Замок для обеспечения observerset не changedd при копировании множества наблюдателей

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 
    } 
    } 
0

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

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

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

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

0

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

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

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

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

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

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

Смежные вопросы