Всем привет, хочу вот поделится наработками, интересно имеет ли право на жизнь такая статья, или смысла нет ибо "еще один велосипед"? Многопоточный Observer на C++ (практика)
Есть много вариаций на тему данного паттерна, но большинство примеров, найденных мною, не подходит для многопоточных приложений.
В этой статье я хочу поделится опытом применения паттерна в многопоточных приложениях и опишу основные проблемы, с которыми мне пришлось столкнуться. Словарь предметной области.
Для начала давайте разберемся со словарем предметной области.
И так, действующие лица
Издатель, рассылающий уведомления — NotificationsDispatcher
Подписчик, получающий уведомления — NotificationsListener
Взаимодействие учасников
Подписка — Subscribe
Прекращение подписки — Unsubscribe
Отправка и получение сообщений — SendNotification и OnNotification
Также необходимо учитывать время жизни объектов-подписчиков и объекта-издателя. Простейшая реализация для однопоточной среды.
class CNotificationListener:
public INotificationListener
{
virtual void OnNotification(void* pContext)
{
wprintf(L"%d\n", *((int*)pContext));
}
};
int _tmain(int argc, _TCHAR* argv[])
{
CNotificationDispatcher aDispatcher;
CNotificationListener aListener1;
CNotificationListener aListener2;
CNotificationListener aListener3;
aDispatcher.Subscribe(&aListener1);
aDispatcher.Subscribe(&aListener2);
aDispatcher.Subscribe(&aListener3);
for(int i = 0; i < 5; ++i)
{
aDispatcher.SendNotification(&i);
}
aDispatcher.Unsubscribe(&aListener2);
aDispatcher.Unsubscribe(&aListener1);
aDispatcher.Unsubscribe(&aListener3);
return 0;
}
Переходим к многопоточной среде.
С одним потоком такой код будет работать довольно стабильно.
Давайте посмотрим что будет при работе нескольких потоков.
Если запустить такой код на выполнение, то рано или позно произойдет креш.
Проблема заключается в добавлении/удалении подписчиков и одновременной рассылке уведомлений (многопоточный доступ к CNotificationDispatcher::m_vListeners в нашем примере).
Здесь необходима синхронизация доступа к списку подписчиков CNotificationDispatcher::m_vListeners. Синхронизация доступа к списку подписчиков.
При такой реализации данный код не будет падать при использовании в предыдущем примере, но нас поджидает еще одна неприятная ситуация. Борьба с взаимной блокировкой потоков (deadlock).
Допустим у нас есть некий поток, выполняющий какую-то фоновую задачу и есть окно, где отображается ход выполнения этой задачи.
Как правило поток посылает уведомление классу окна, который в свою очередь вызывает SendMessage и делает какие-то действия в оконной процедуре.
Функция SendMessage является блокирующей, и работает она следующим образом — посылает уведомление потоку окна и ждет пока тот его обработает.
Если подключение/отключение подписчика будет происходить так-же в оконной процедуре (в контексте потока окна) возможна взаимная блокировка потоков, так называемый deadlock.
Такой deadlock может воспроизоводится крайне редко (в момент вызова Subscribe/Unsubscribe и одновременном вызове OnNotification в отдельном потоке)
Следующий код эмулирует данный deadlock.
Проблема заключается в том, что главный поток захватывает глобальную критическую секцию g_cs (при аналогии с оконной процедурой — выполняется в контексте оконного потока), и затем вызывает метод Subscribe/Unsubscribe, который внутри захватывает CNotificationDispatcher::m_cs.
В этот момент рабочий поток посылает уведомление, захватив CNotificationDispatcher::m_cs, и затем пытается захватить глобальный g_cs (при аналогии с оконом — вызывает SendMessage).
Поток окна A -> B
Рабочий поток B -> A
Это можно назвать класическим deadlock-ом.
Проблема скрывается в реализации метода CNotificationDispatcher::SendNotification
Мы не должны отсылать уведомления (вызывать callback), захватив при этом какой-либо синхронизационный объект, котрый может быть захвачен в обработчике уведомления (напрямую или косвенно). В реальном проекте таких ситуаций может быть множество, и разобраться с ними порой довольно сложно. И так, убираем блокировку при вызове уведомлений
Контроль времени жизни подписчиков.
После того, как мы убрали deadlock при вызове функции OnNotification у нас появилась другая проблема — время жизни объектов-подписчиков.
У нас больше нет гарантии, что метод OnNotification не будет вызван после вызова Unsubscribe, и по этому мы не можем удалить объект-подписчик непосредственно после вызова Unsubscribe.
В данной ситуации проще всего контролировать время жизни объектов-подписчиков с использованием счетчика ссылок.
Для этого можно исползовать технологию COM — унаследовать интерфейс INotificationListener от IUnknown и использовать ATL CComPtr для списка подписчиков внутри класса CNotificationDispatcher, тоесть заменить std::vector<INotificationListener*> на std::vector<CComPtr<INotificationListener>>.
Но такая реализация чревата дополнительными расходами на реализацию классов-подписчиков, так как в каждом из них должны быть реализованы методы AddRef/Release.
Для контроля времени жизни подписчиков с исползованием счетчика ссылок хорошо подойдут умные указатели. Финальная версия.
Нижележащий код будет финальной версией в данном обзоре, но это далеко не идеальная реализация, так как нет пределу совершенства.
Также в каждом конкретном случае возможны вариации, универсальное решение не всегда лучше.
Я заменил "голый" указатель INotificationListener* на "умный" указатель со счетчиком ссылок, такой оказался в библиотеке boost.
В функции Unsubscribe входной параметр используется исключительно как идентификатор отключаемого объекта, по этому там можно оставить просто указатель.
Также в функции Unsubscribe я добавил переменную toRelease для того, чтобы вызвать деструктор подписчика уже после вызова Unlock
Стоит обратить внимание на то, что в функции SendNotification происходит копирование списка умных указателей (после копирования все указатели увеличивают свои счетчики ссылок, а при выходе из функции уменьшают, что и контролирует время жизни подписчиков) Тестируем.
Здравствуйте, Ryadovoy, Вы писали:
R>Всем привет, хочу вот поделится наработками, интересно имеет ли право на жизнь такая статья, или смысла нет ибо "еще один велосипед"? R>...
Общая идея понятна. Детально код не изучал, но что сразу резануло по глазам — небезопасность кода с точки зрения исключений — повсеместно в коде встречаются фрагменты:
EnterCriticalSection();
//Какие-то операции
LeaveCriticalSection();
Что если во время выполнения "каких-то операций" возникнет исключение? Почему бы в данном случае не использовать RAII — например, мьютексы и локи из boost::thread?
Здравствуйте, rg45, Вы писали:
R>Общая идея понятна. Детально код не изучал, но что сразу резануло по глазам — небезопасность кода с точки зрения исключений — повсеместно в коде встречаются фрагменты: R>
R>EnterCriticalSection();
R>//Какие-то операции
R>LeaveCriticalSection();
R>
R>Что если во время выполнения "каких-то операций" возникнет исключение? Почему бы в данном случае не использовать RAII — например, мьютексы и локи из boost::thread?
Вы конечно-же со мной не согласитесь, но я считаю что c++ исключения это зло.
Я работаю в команде, где не особо ими пользуются (только при необходимости), а необходимости из под колбека выбрасывать исключение нет никакой.
bools и stl может выбросить исключение, но оно возможно лишь когда имеет место ошибка программиста
(ну или закончилась память в системе, что тоже критично), тогда нужно падать с креш репортом, дабы не скрывать ошибку.
Здравствуйте, Alexander G, Вы писали: AG>ну, когда мне такое понадобится, я скорее посмотрю на boost::signals2
Спасибо за наводку, я обязательно обращу внимание на signals2.
Моя статья направлена не на то, чтобы дать готовое решение,
она объясняет основные принципы, которые применимы там, где boost бывает недоступен (драйвера, нейтивные приложения и т.п.)
Здравствуйте, Ryadovoy, Вы писали:
R>Всем привет, хочу вот поделится наработками, интересно имеет ли право на жизнь такая статья, или смысла нет ибо "еще один велосипед"?
статья актуальна, поэтому я поддерживаю вашу инициативу
есть ряд замечаний, которые, как мне кажется, вполне уместны
1) в коде много Windows-specific кода, мне кажется, что это отвлекает от сути подхода
общеупотребительные (типа буста) или ваши собственные абстракции, скрывающие взаимодействие с операционкой были бы уместны.
пример:
class Mutex
{
public:
void Lock();
void Unlock();
};
class MutexLock //: non_copyable
{
public:
explicit MutexLock(Mutex& m)
: M(m)
{
M.Lock();
}
~MutexLock()
{
M.Unlock();
}
private:
Mutex& M;
};
//usage:
Mutex m; // guard for data
MutexLock lock(m); //RAII
modify data
2) вам посоветовали исключить ручные Lock\Unlock, потому что это усложняет поддержку кода и может привести к проблемам в случае исключений имхо (хотя в конкретном коде я проблем не увидел)
3) передача this изнутри класса может привести к непредсказуемым результатам и вы это совсем не сразу обнаружите. если класс регистрировали, имея один указатель, то он может не совпадать с this внутри этого класса (представьте множественные и виртуальные наследования). тут можно опираться на некие ID в самом классе (гуиды или автоинкременты), либо ограничить использование. просто об этом не стОит забывать
4) в паттерне observer еще есть такой подход: класс, за которым наблюдают хранит в себе список weak_ptr на обзерверы. не всегда логично, что этот объект должен управлять временем жизни наблюдателя. таким образом алгоритм усложняется тем, что надо иногда из списка удалять мертвые ссылки и Unregister становится ненужным\необязательным.
успехов
Здравствуйте, remark, Вы писали:
R>Это, конечно, достаточно печально. Выделять память, копировать N памяти, атомарно инкрементировать N счётчиков под мьютексом не айс.
ничего печального нет в этом
просто вы озабочены лок-фри алгоритмами и оптимизациями
статья далека от ваших заумностей, но вполне подходит для решения простых задач даже в корпоративном софте
Здравствуйте, uzhas, Вы писали:
U>Здравствуйте, Ryadovoy, Вы писали: U>успехов
совсем забыл
5) никогда не забывайте объявлять виртуальный деструктор в интерфейсах
U>2) вам посоветовали исключить ручные Lock\Unlock, потому что это усложняет поддержку кода и может привести к проблемам в случае исключений имхо (хотя в конкретном коде я проблем не увидел)
Теоритически эти проблемы могут возникнуть при возбуждении std::bad_alloc при вставке в вектор (CNotificationDispatcher::Subscribe) :
Правильно понял, что signals2 впервые появились в мае 2009-го года,
и до этого были лишь signals, которые были не устойчивы к многопоточности?
Есть ли еще какие-нибудь альтернативы?
Здравствуйте, uzhas, Вы писали:
R>>Это, конечно, достаточно печально. Выделять память, копировать N памяти, атомарно инкрементировать N счётчиков под мьютексом не айс. U>ничего печального нет в этом U>просто вы озабочены лок-фри алгоритмами и оптимизациями U>статья далека от ваших заумностей, но вполне подходит для решения простых задач даже в корпоративном софте
Это не повод заниматься предварительной пессимизацией и неправильно проектировать.
Один подписчик обычно обрабатывает множество уведомлений. Отсюда следует, что копировать контейнер и захватывать ссылки лучше при добавлении/удалении подписчиков, а не при нотификациях. Соотв. ты просто делаешь что-то типа такого. И никакой тебе лок-фри магии. Нотификации всегда O(1).
Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>Теоритически эти проблемы могут возникнуть при возбуждении std::bad_alloc при вставке в вектор (CNotificationDispatcher::Subscribe) :
Если необходима обработка нехватки памяти, это делается, но не для примера в статье, вы со мной не согласны?
Наши user-level приложения просто падают когда заканчивается память, и создается креш дамп, ибо странно что закончилась память, неправда-ли?
В большинстве случаев это случается из-за нашей внутренней ошибки (например heap corraption или что-то в этом роде).
В таком случае лучше сразу-же увалить программу и пусть сгенерирует креш дамп, который мы проанализируем и пофиксим баг.
Какой смысл от неотпущенного лока если мы получили критическую ошибку?
Здравствуйте, Ryadovoy, Вы писали:
R>Наши user-level приложения просто падают когда заканчивается память, и создается креш дамп, ибо странно что закончилась память, неправда-ли? R>В большинстве случаев это случается из-за нашей внутренней ошибки (например heap corraption или что-то в этом роде). R>В таком случае лучше сразу-же увалить программу и пусть сгенерирует креш дамп, который мы проанализируем и пофиксим баг. R>Какой смысл от неотпущенного лока если мы получили критическую ошибку?
Если ты в фотошопе редактируешь большую картинку, и начинаешь применять какой-то сложный фильтр, какой вариант развития событий ты выберешь?
1. Фотошоп падает и теряет твои изменения.
2. Фотошоп откатывает применение фильтра, как будто он не начинался, и выдаёт окошко, что операция прервана из-за нехватки памяти.
Здравствуйте, remark, Вы писали:
R>Это не будет работать. Достаточно в обработчике добавить/удалить несколько подписчиков, как всё крякнется.
Идея была сделать устойчивую финальную версию,
промежуточные версии нужны лишь для объяснения проблеммы, и они не тестировались в различных ситуациях.
Или вы имели в виду финальную версию?
Здравствуйте, remark, Вы писали:
R>Здравствуйте, Ryadovoy, Вы писали:
R>>Наши user-level приложения просто падают когда заканчивается память, и создается креш дамп, ибо странно что закончилась память, неправда-ли? R>>В большинстве случаев это случается из-за нашей внутренней ошибки (например heap corraption или что-то в этом роде). R>>В таком случае лучше сразу-же увалить программу и пусть сгенерирует креш дамп, который мы проанализируем и пофиксим баг. R>>Какой смысл от неотпущенного лока если мы получили критическую ошибку?
R>Если ты в фотошопе редактируешь большую картинку, и начинаешь применять какой-то сложный фильтр, какой вариант развития событий ты выберешь? R>1. Фотошоп падает и теряет твои изменения. R>2. Фотошоп откатывает применение фильтра, как будто он не начинался, и выдаёт окошко, что операция прервана из-за нехватки памяти.
И дальше подварианты:
2.1. Фотошоп потом наглухо задедлочится при следующей операции
2.2. Можно будет нормально сохранить изображение, и продолжить работу, когда память освободится
?
Здравствуйте, remark, Вы писали:
R>Это не повод заниматься предварительной пессимизацией и неправильно проектировать.
вместо одного push_back вы сделали три операции, на осознание которых (честно) у меня ушло 3 минуты
имхо, это называется предварительная оптимизация
да и употребление .px вместо .get() тоже намекает на вашу озабоченность
просто вы так долго варитесь в этом, что считаете это естественным, тогда как другие чаще всего работают на другом уровне
но я не хочу спорить об этом R>Один подписчик обычно обрабатывает множество уведомлений. Отсюда следует, что копировать контейнер и захватывать ссылки лучше при добавлении/удалении подписчиков, а не при нотификациях. Соотв. ты просто делаешь что-то типа такого. И никакой тебе лок-фри магии. Нотификации всегда O(1).
я понял вашу идею, она мне тоже кажется оптимальнее
R>
Здравствуйте, remark, Вы писали:
R>Если ты в фотошопе редактируешь большую картинку, и начинаешь применять какой-то сложный фильтр, какой вариант развития событий ты выберешь? R>1. Фотошоп падает и теряет твои изменения. R>2. Фотошоп откатывает применение фильтра, как будто он не начинался, и выдаёт окошко, что операция прервана из-за нехватки памяти.
Программы и ситуации бывают разными.
Мне попадался проект в котором сплошь и рядом встречались конструкции try{}catch(...){} которые прятали реальные баги.
программа не падала, но глючила страшно.
Удалось ее нам привести к более менее стабильному состоянию лишь убрав все эти перехватчики.
Здравствуйте, Ryadovoy, Вы писали:
R>Идея была сделать устойчивую финальную версию, R>промежуточные версии нужны лишь для объяснения проблеммы, и они не тестировались в различных ситуациях. R>Или вы имели в виду финальную версию?
Я имею в виду первую версию. Я хотел сказать, что проблема не столько связана с многопоточностью. Копирование или какой-то другой механизм появился бы у нас ещё в однопоточной версии.
Здравствуйте, Ryadovoy, Вы писали:
R>Мне попадался проект в котором сплошь и рядом встречались конструкции try{}catch(...){} которые прятали реальные баги. R>программа не падала, но глючила страшно. R>Удалось ее нам привести к более менее стабильному состоянию лишь убрав все эти перехватчики.
Бесспорно бывают решения ещё хуже.
Вопрос только, сравнить свою программу с гораздо более плохой и сказать "Ну ничего у меня ещё не худший вариант". Или сравнить свою программу с более хорошей и сказать "А ведь можно сделать лучше".
Здравствуйте, Ryadovoy, Вы писали:
R>Правильно понял, что signals2 впервые появились в мае 2009-го года, R>и до этого были лишь signals, которые были не устойчивы к многопоточности?
Да.
R>Есть ли еще какие-нибудь альтернативы?
Думаю, есть.
Я не интересовался, мне не был нужен многопоточный обсервер. (Вообще, мне кажется, трудно быть уверенным в потокобезопасности, когда не уверен, сколько и каких коллбэков вызываются, даже если сама реализация обсервера безопасна.)
Здравствуйте, remark, Вы писали:
R>Это, конечно, достаточно печально. Выделять память, копировать N памяти, атомарно инкрементировать N счётчиков под мьютексом не айс.
В драйверах, где мне необходима была оптимизация, я избавился от копирования вектора, но та схема была на порядок сложнее.
Там было что-то вроде связного списка, который перебирается под локом (SpeenLock) и инкрементировался счетчик ссылок, а сам вызов происходил уже без лока.
И удаление ячейки из этого списка происходило тоже под этим локом, еще для чего-то была нужна переменная bRemove, уже не припомню.
Но там еще все это усугублялось необходимостью работать на разных уровнях IRQL (PASSIVE_LEVEL, DISPATCH_LEVEL).
Я хотел предоставить схему, которая будет предельно понятна, т.к. тогда при реализации такой схемы меньше шансов допустить ошибку.
Здравствуйте, Ryadovoy, Вы писали:
R>Если необходима обработка нехватки памяти, это делается, но не для примера в статье, вы со мной не согласны?
мне кажется, что зря вы так относитесь к своим "критикам", вам нужно быть сдержанным и мотать на ус ;=)
некоторые вещи режут глаза опытным разработчикам, потому что у них есть хороший багаж опыта, много книжек всяких прочитали от других профи
даже если вы не переносите исключения, то вы все равно могли бы сделать код более качественным с точки зрения поддержки
в этом вам помог бы механизм RAII
он удобен как при исключениях, так и исключение копи-паста, уменьшение _незначащего_ кода
язык С++ достаточно низкоуровневый и очень часто сложно за деревьями увидеть лес, то есть мелочи отвлекают и мешают понять суть
выработаны практики, который частично решают эти проблемы языка и вам их советуют
даже если вы лично не работаете с исключениями. вы могли бы написать код для клиентов, которые работают с ними
и ваш код не пострадал бы
вы могли заметить, что в некоторых функциях перед return у вас стоит Unlock
это и есть проблема в развивающемся коде (то есть не одноразовом коде студента, а коде, который используется\развивается в компании\команде из большого кол-ва разработчиков)
код в большой компании читают очень много, и он должен быть понятен
можно забыть сделать Unlock и это усложняет поддержку кода
именно поэтому в C использовали идиому single function exit point : http://c2.com/cgi/wiki?SingleFunctionExitPoint (чтобы в самом конце освобождать ресурсы)
в C++ это решили через RAII (инструмент не идеальный, ибо плохо дружит с исключениями, но это отдельная холиворная тема, которые тут уже не раз перемалывали)
попробуйте хотя бы частично всосать те советы, которые вам дают
успехов
Здравствуйте, Ryadovoy, Вы писали:
ЮЖ>>Теоритически эти проблемы могут возникнуть при возбуждении std::bad_alloc при вставке в вектор (CNotificationDispatcher::Subscribe) :
R>Если необходима обработка нехватки памяти, это делается, но не для примера в статье, вы со мной не согласны?
Здесь не нужна обработка нехватки памяти. Только RAII для lock/unlock. Как миниму в силу того, что на момент написания неизвестны сценарии использования.
R>Наши user-level приложения просто падают когда заканчивается память, и создается креш дамп, ибо странно что закончилась память, неправда-ли?
Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>Здесь не нужна обработка нехватки памяти. Только RAII для lock/unlock. Как миниму в силу того, что на момент написания неизвестны сценарии использования.
Рои не панацея, и иногда уменьшают читабельность и понимание происходящего.
Здравствуйте, uzhas, Вы писали:
U>мне кажется, что зря вы так относитесь к своим "критикам", вам нужно быть сдержанным и мотать на ус ;=)
Если я был с кем-то резок, прошу извинения, я просто пытаюсь поддерживать беседу.
U>некоторые вещи режут глаза опытным разработчикам, потому что у них есть хороший багаж опыта, много книжек всяких прочитали от других профи
Опыт может быть в разных областях программирования, использования разных библиотек и т.п.
и соответственно глаза резать могут разные вещи в зависимости от того, в какой области программирования кто работает,
и с какими библиотеками кто привык работать, даже от соглашения оформления кода может много чего зависить.
Давайте в конце концов не будем меряться опытом?
U>даже если вы не переносите исключения, то вы все равно могли бы сделать код более качественным с точки зрения поддержки U>в этом вам помог бы механизм RAII
Этот механизм все же "на любителя"
U>код в большой компании читают очень много, и он должен быть понятен
Механизм RAII не улучшает читабельность кода, и понимание его, что более важно для статьи.
Здравствуйте, Ryadovoy, Вы писали:
ЮЖ>>Здесь не нужна обработка нехватки памяти. Только RAII для lock/unlock. Как миниму в силу того, что на момент написания неизвестны сценарии использования.
R>Рои не панацея, и иногда уменьшают читабельность и понимание происходящего.
Каким образом переписывание такого кода на вариант с использованием RAII уменьшит читабельность?
+ потенциальных мест для ошибок в этом коде больше чем в аналоге с RAII.
+
Если тебе мешает дополнительный scope, то можно переписать код так:
R>void SendNotification(void* pContext)
{
ScopeLocker scopeLocker(&m_cs);
CNotificationListenerPtrList vListeners = m_vListeners;
scopeLocker.release(); // or you may call it "unlock"for(size_t i = 0; i < vListeners.size(); ++i)
{
vListeners[i].OnNotification(pContext);
}
}
Здравствуйте, Ryadovoy, Вы писали:
U>>код в большой компании читают очень много, и он должен быть понятен R>Механизм RAII не улучшает читабельность кода, и понимание его, что более важно для статьи.
Однажды мне попался код из проекта одного уважаемого разработчика:
Само собой, это упрощенная схема — реальный код был более развесистый. Память не текла лишь при удачном стечении обстоятельств. После замены на RAII оно стало выглядеть вот так:
GuardResource1();
if (!CheckResource1())
return false;
GuardResource2();
if (!CheckResource2())
return false;
GuardResource3();
if (!CheckResource3())
return false;
...
return true;
Конечно, всех проблем это не решило, но темпы утечек серьезно уменьшились. Пострадала ли читабельность?
Здравствуйте, Ryadovoy, Вы писали:
R>Здравствуйте, rg45, Вы писали:
R>>Общая идея понятна. Детально код не изучал, но что сразу резануло по глазам — небезопасность кода с точки зрения исключений — повсеместно в коде встречаются фрагменты: R>>
R>>EnterCriticalSection();
R>>//Какие-то операции
R>>LeaveCriticalSection();
R>>
R>>Что если во время выполнения "каких-то операций" возникнет исключение? Почему бы в данном случае не использовать RAII — например, мьютексы и локи из boost::thread?
R>Вы конечно-же со мной не согласитесь, но я считаю что c++ исключения это зло.
а можно узнать почему??? какие ваши аргументы?
R>Я работаю в команде, где не особо ими пользуются (только при необходимости),
мда бывает неприятность... ;( мои соболезнования
R>а необходимости из под колбека выбрасывать исключение нет никакой. R>bools и stl может выбросить исключение, но оно возможно лишь когда имеет место ошибка программиста R>(ну или закончилась память в системе, что тоже критично), тогда нужно падать с креш репортом, дабы не скрывать ошибку.
для 90х это было бы ОК, но на дворе-то 2010й
зачем было разводить столько графоманства, когда современными средствами задача решается тривиально?
пруф-код, слепленый за 5 ммин
struct Subscriber {
void OnSomething(int asContext) { printf("%i\n", asContext); }
};
int _tmain(int argc, _TCHAR* argv[])
{
std::set<Subscriber*> subscribers;
subscribers.insert(new Subscriber());
int context = 7;
task_group g;
g.run( [subscribers,context] { for each(auto s in subscribers) { s->OnSomething(context); }} );
g.wait();
return 0;
}
Здравствуйте, Ryadovoy, Вы писали:
U>>даже если вы не переносите исключения, то вы все равно могли бы сделать код более качественным с точки зрения поддержки U>>в этом вам помог бы механизм RAII R>Этот механизм все же "на любителя"
Это общепринятый механизм.
Ручные EnterCriticaLSection, LeaveCriticalSection и иже с ними у нас являются поводом к прекращени code review и отправкой его на доработку.
Здравствуйте, Ryadovoy, Вы писали:
R>Здравствуйте, Alexander G, Вы писали: AG>>ну, когда мне такое понадобится, я скорее посмотрю на boost::signals2 R>Спасибо за наводку, я обязательно обращу внимание на signals2.
Имхо, надо было с этого начать. В диссертациях не зря первая глава всегда — это обзор существующих наработок по теме.
R>Моя статья направлена не на то, чтобы дать готовое решение, R>она объясняет основные принципы, которые применимы там, где boost бывает недоступен (драйвера, нейтивные приложения и т.п.)
с чего бы это буст был там недоступен? и что такое "нейтивные приложения" в контексте С++?
Здравствуйте, jazzer, Вы писали:
J>Имхо, надо было с этого начать. В диссертациях не зря первая глава всегда — это обзор существующих наработок по теме.
Спасибо за замечание, я думаю добавить в статью что-то вроде этого:
По сути я не выдумывал новое решение, я использовал готовый шаблон проектирования,
этот же шаблон использовали и создатели boost, но они разрабатывают именно библиотеку, а не решение для частной ситуации.
boost это огромное количество кода, такое количество кода необходимо, если писать универсальную библиотеку, для частного решения может подойти более простой вариант,
и из-за этого он будет более понятным и легко реализуемым.
Вы досконально понимаете как устроена библиотека signal2 в boost?
лично меня пугает количество кода при вызове колбека, вот стек вызовов.
J>с чего бы это буст был там недоступен? и что такое "нейтивные приложения" в контексте С++?
Ну я считаю что многопоточность это уже не совсем контекст c++, возможно я выбрал не совсем ту ветку форума, но более подходящей не нашел.
Native приложение это приложение, которое может запускатья на ранней стадии загрузки виндовс (например консоль восстановления)
виндовый checkdisk так работает, он запускает c:\WINDOWS\system32\autochk.exe на ранней стадии загрузки. autochk.exe это и есть Native приложение.
Native приложение в основном пользуется лишь экспортом из ntdll.dll (недокументированными функциями), и ничего другого ему не доступно.
У нас по началу были большие трудности с CRT и STL, а вы про boost говорите...
Я Windows программист, и никогда не занимался вопросами переносимости кода на другие платформы,
но думаю что и на других платформах существуют схожие проблемы.
Интересно можно ли при написании модулей ядра в Linux пользоваться библиотеками STL и boost?
Я думаю что все зависит от кривизны рук, а не от применяемого инструмента или от технологии.
Если разработчик с кривыми руками нужно его гнать в шею и нанять толкового программиста.
Не думайте что я не буду использовать RAII если они будут нужны,
как доказательство могу привести пример моего личного их использования
for(;;)
{
{ //Enter to notify thread critical section
CGuard aGuard(m_csNotifyThread);
if(m_vFilesNotifyQueue.empty())
{
if(!str)
{
//Scan was completed
CGuard aGuard(m_csScanState);
m_nScanState = IFRClient::NotStarted;
}
break;
}
}
if(WaitForSingleObject(m_hNotifyThread, 100) != WAIT_TIMEOUT)
{
//Something happends with notitification thread, that is error case
_ASSERT(FALSE);
CGuard aGuard(m_csScanState);
m_nScanState = IFRClient::NotStarted;
break;
}
}
Здравствуйте, landerhigh, Вы писали:
L>Здравствуйте, Ryadovoy, Вы писали:
U>>>даже если вы не переносите исключения, то вы все равно могли бы сделать код более качественным с точки зрения поддержки U>>>в этом вам помог бы механизм RAII R>>Этот механизм все же "на любителя"
L>Это общепринятый механизм.
L>Ручные EnterCriticaLSection, LeaveCriticalSection и иже с ними у нас являются поводом к прекращени code review и отправкой его на доработку.
Я думаю что тематика RAII к данной статье притянута за уши, давайте создадим тему RAII и будем это там это обсуждать?
я так понимаю это вопрос веры для многих поклонников RAII,
по этому я подумаю над тем, чтобы добавить RAII в примеры,
дабы избежать в дальнейшем коментариев насчет RAII.
Здравствуйте, Ryadovoy, Вы писали:
R>Вы конечно-же со мной не согласитесь, но я считаю что c++ исключения это зло. R>Я работаю в команде, где не особо ими пользуются (только при необходимости), а необходимости из под колбека выбрасывать исключение нет никакой. R>bools и stl может выбросить исключение, но оно возможно лишь когда имеет место ошибка программиста R>(ну или закончилась память в системе, что тоже критично), тогда нужно падать с креш репортом, дабы не скрывать ошибку.
Если класс представлен здесь, значит его предлагается использовать, что в свою очередь влечёт некорректность навязывания своего стиля пользователям.
По-моему, если
1) корректно разрешите проблему безопасности исключений
2) избавитесь от связи (время жизни диспетчера) == (время, проведенное в критической секции)
3) гарантируете отсутствие утечки ресурса "объект-синхронизации" (сейчас это критическая секция)
за приведенный код Вам можно будет сказать спасибо.
Здравствуйте, rm822, Вы писали:
R>для 90х это было бы ОК, но на дворе-то 2010й R>зачем было разводить столько графоманства, когда современными средствами задача решается тривиально? R>пруф-код, слепленый за 5 ммин
R>
Необходимо было решить задачу следующего типа:
Есть работающий поток, выполняющий какую-то фоновую задачу.
Используя шаблон "наблюдатель" поток периодически отсылает какие-то уведомления.
Необходимо поключится "на ходу" к этому потоку и обрабатывать уведомления.
Притом подключение/отключение должно происходить в отдельном потоке и не вызывать сбоев.
Дополнительно необходимо поддерживать отключение от нотификации при получении этой самой нотификации (прямо из callback-а)
Такую задачу без использования методов синхронизации не решить.
Здравствуйте, Ryadovoy, Вы писали:
R>Вы досконально понимаете как устроена библиотека signal2 в boost? R>лично меня пугает количество кода при вызове колбека, вот стек вызовов.
Это стек вызовов отладочной сборки, я так понимаю?
J>>с чего бы это буст был там недоступен? и что такое "нейтивные приложения" в контексте С++? R>Ну я считаю что многопоточность это уже не совсем контекст c++, возможно я выбрал не совсем ту ветку форума, но более подходящей не нашел.
Если не интересует многопоточность, то есть Boost.Signals первой версии.
У нее проблемы с многопоточностью (как раз с отпиской клиентов), но проще реализация. Так что если не предполагается гонять все это в нескольких потоках (хотя у вас же там критические секции сплошь), то можно использовать ее.
R>Native приложение это приложение, которое может запускатья на ранней стадии загрузки виндовс (например консоль восстановления) R>виндовый checkdisk так работает, он запускает c:\WINDOWS\system32\autochk.exe на ранней стадии загрузки. autochk.exe это и есть Native приложение. R>Native приложение в основном пользуется лишь экспортом из ntdll.dll (недокументированными функциями), и ничего другого ему не доступно. R>У нас по началу были большие трудности с CRT и STL, а вы про boost говорите...
Хм.. Какие именно трудности с STL? CRT — я еще могу понять, но STL — это же библиотека шаблонов Она сама по себе вообще ничего стороннего не зовет, кроме new/delete (и то, почти везде можно отдать свой аллокатор вместо стандартного, и тогда вообще прогулок в CRT не будет).
R>Интересно можно ли при написании модулей ядра в Linux пользоваться библиотеками STL и boost?
С этими вопросами вам лучше в "Низкоуровневое программирование".
Но я видел в англоязычных блогах сообщения, народ юзал буст для драйверов и был очень доволен.
Правильно ли я понимаю, что в вашей модели, уже отписавшийся подписчик может получить уведомление?
В самом деле, в методе SendNotification вы делаете копию массива подписок под критической секцией.
После чего у вас некоторый слушатель может отписаться, но все равно получить уведомление.
Это не очень хорошо.
Здравствуйте, baily, Вы писали:
B>Здравствуйте, Ryadovoy, Вы писали:
B>Правильно ли я понимаю, что в вашей модели, уже отписавшийся подписчик может получить уведомление? B>В самом деле, в методе SendNotification вы делаете копию массива подписок под критической секцией. B>После чего у вас некоторый слушатель может отписаться, но все равно получить уведомление. B>Это не очень хорошо.
Еще мне не нравится, что метод Unsubscribe принимает на вход указатель-подписчика. Получается, что тот, кто отписывает, должен иметь этот указатель. Это не очень удобно. Обычно в таких случаях метод Subscribe возвращает некоторый дескриптор, который потом надо передавать в Unsubscribe. Наример, можно сделать так:
Здравствуйте, baily, Вы писали:
B>Здравствуйте, Ryadovoy, Вы писали:
B>Правильно ли я понимаю, что в вашей модели, уже отписавшийся подписчик может получить уведомление? B>В самом деле, в методе SendNotification вы делаете копию массива подписок под критической секцией. B>После чего у вас некоторый слушатель может отписаться, но все равно получить уведомление. B>Это не очень хорошо.
Можно попробовать ожидать завершение текущего уведомления в функции Unsubscribe, но тогда скорей всего будут зависания при отключении.
Я пока что не нашел подходящее решение с реализацией Unsubscribe, гарантирующей прекращение нотификаций после выхода.
Если есть какие-то идеи по этому поводу, буду рад услышать.
Здравствуйте, Ryadovoy, Вы писали:
R>Можно попробовать ожидать завершение текущего уведомления в функции Unsubscribe, но тогда скорей всего будут зависания при отключении. R>Я пока что не нашел подходящее решение с реализацией Unsubscribe, гарантирующей прекращение нотификаций после выхода. R>Если есть какие-то идеи по этому поводу, буду рад услышать.
Это фундаметальная проблема такого подхода.
Решить её можно только перейдя к асинхронному обмену сообщениями. А именно с каждым объектом связывается понятие родительского потока, при нотификации родительским потокам рассылаются асинхронные сообщения. При их получении потоки проверяют, не отписан ли ещё каждый конкретный объект, если нет, то вызывается обработчик объекта. Соотв. отписывать можно только из родительского потока.
Здравствуйте, remark, Вы писали:
R>Это фундаметальная проблема такого подхода. R>Решить её можно только перейдя к асинхронному обмену сообщениями. А именно с каждым объектом связывается понятие родительского потока, при нотификации родительским потокам рассылаются асинхронные сообщения. При их получении потоки проверяют, не отписан ли ещё каждый конкретный объект, если нет, то вызывается обработчик объекта. Соотв. отписывать можно только из родительского потока.
Это так же избавит от необходимости использования каких-либо мьютексов.
Здравствуйте, Ryadovoy, Вы писали:
R>Здравствуйте, baily, Вы писали:
B>>Здравствуйте, Ryadovoy, Вы писали:
B>>Правильно ли я понимаю, что в вашей модели, уже отписавшийся подписчик может получить уведомление? B>>В самом деле, в методе SendNotification вы делаете копию массива подписок под критической секцией. B>>После чего у вас некоторый слушатель может отписаться, но все равно получить уведомление. B>>Это не очень хорошо.
R>Можно попробовать ожидать завершение текущего уведомления в функции Unsubscribe, но тогда скорей всего будут зависания при отключении. R>Я пока что не нашел подходящее решение с реализацией Unsubscribe, гарантирующей прекращение нотификаций после выхода. R>Если есть какие-то идеи по этому поводу, буду рад услышать.
Есть много разных способов. Например, можно связать с каждым подписчиком класс-защелку. К ней должен иметь доступ как подписчик, так и уведомитель.
Защелка открывается в методе Subscribe после добавление подписчика. В методе Unsubscribe она закрывается уведомителем под критической секцией.
Класс подписчик в своем методе OnNotification должен вначале проверять, разрешен ли ему вызов. Если нет, то просто выходить.
Защелкой должен владеть подписчик. Можно просто создать обертку над подписчиком ( в принципе и умный указатель есть обертка ), который владеет и подписчиком
и защелкой. В более сложном варианте можно сделать защелку более сложной, поместив туда еще и событие. При вызове OnNotification, когда защелка открыта,
можно увеличивать счетчик использования защелки в начале и уменьшать в конце. А событие взводить, когда счетчик станет равным нулю. Тогда на событии можно ожидать,
когда подписчик закончит обрабатывать свой OnNotification.
Я в свое время много потрудился над данной задачей. Но у меня требования были пожестче. Там есть немало подводных камней, хотя на вид она кажется простой.
Здравствуйте, remark, Вы писали:
R>Здравствуйте, Ryadovoy, Вы писали:
R>>Можно попробовать ожидать завершение текущего уведомления в функции Unsubscribe, но тогда скорей всего будут зависания при отключении. R>>Я пока что не нашел подходящее решение с реализацией Unsubscribe, гарантирующей прекращение нотификаций после выхода. R>>Если есть какие-то идеи по этому поводу, буду рад услышать.
R>Это фундаметальная проблема такого подхода. R>Решить её можно только перейдя к асинхронному обмену сообщениями. А именно с каждым объектом связывается понятие родительского потока, при нотификации родительским потокам рассылаются асинхронные сообщения. При их получении потоки проверяют, не отписан ли ещё каждый конкретный объект, если нет, то вызывается обработчик объекта. Соотв. отписывать можно только из родительского потока.
R>
Именно. Если разрешать отписку в методе OnNotification, а это, в принципе, довольно естественное требование, то поток, рассылающий уведомления, должен вызывать уведомления асинхронно.
Здравствуйте, baily, Вы писали:
B>Есть много разных способов. Например, можно связать с каждым подписчиком класс-защелку. К ней должен иметь доступ как подписчик, так и уведомитель. B>Защелка открывается в методе Subscribe после добавление подписчика. В методе Unsubscribe она закрывается уведомителем под критической секцией. B>Класс подписчик в своем методе OnNotification должен вначале проверять, разрешен ли ему вызов. Если нет, то просто выходить. B>Защелкой должен владеть подписчик. Можно просто создать обертку над подписчиком ( в принципе и умный указатель есть обертка ), который владеет и подписчиком B>и защелкой. В более сложном варианте можно сделать защелку более сложной, поместив туда еще и событие. При вызове OnNotification, когда защелка открыта, B>можно увеличивать счетчик использования защелки в начале и уменьшать в конце. А событие взводить, когда счетчик станет равным нулю. Тогда на событии можно ожидать, B>когда подписчик закончит обрабатывать свой OnNotification.
Спасибо за идею, надо будет поразмышлять над ней...
B>Я в свое время много потрудился над данной задачей. Но у меня требования были пожестче. Там есть немало подводных камней, хотя на вид она кажется простой.
Здравствуйте, remark, Вы писали:
R>Это фундаметальная проблема такого подхода. R>Решить её можно только перейдя к асинхронному обмену сообщениями. А именно с каждым объектом связывается понятие родительского потока, при нотификации родительским потокам рассылаются асинхронные сообщения. При их получении потоки проверяют, не отписан ли ещё каждый конкретный объект, если нет, то вызывается обработчик объекта. Соотв. отписывать можно только из родительского потока.
Не совсем понял идею родительских потоков (это как?)
Мне кажется не всегда удобно пользоваться асинхронными сообщениями, например если надо у пользователя спросить что-то и в зависимости от ответа продолжить или остановится, с асинхронными сообщениями это будет сделать сложнее. Еще проще для понимания фиксированные состояния системы, если поток ожидает пока пройдет нотификация проще понимать что происходит в системе (проще фиксить баги в некоторых случаях к примеру)
Если выбирать между гарантией неполучения сообщения после вызова Unsubscribe в синхронных сообщениях и асинхронными сообщениями я скорее выбрал бы синхронные.
Хочу спросить у форумчан: на сколько критично автоматическая отписка при уничтожении подписчиков или издателя?
Плюсы:
— программисту не надо постоянно следить за подписками, при уничтожении подписчика, объект сам отпишется от издателя (издатель отпишет/уведомит подписчиков соответственно);
— код бизнес логики не изобилует дополнительными проверками.
Минусы:
— дополнительный расход памяти на дополнительные списки (к каким издателям подписан подписчик), для встраиваемых систем может быть критично;
— расслабляет программиста, вместо продуманного алгоритма с четким временем жизни объектов, чаще пишется код по принципу "заработало и ладно".
Все выше сказанное — мое ИМХО, но жду ваших конструктивных ответов.
Здравствуйте, Ryadovoy, Вы писали:
R>Здравствуйте, remark, Вы писали:
R>Мне кажется не всегда удобно пользоваться асинхронными сообщениями, например если надо у пользователя спросить что-то и в зависимости от ответа продолжить или остановится, с асинхронными сообщениями это будет сделать сложнее. Еще проще для понимания фиксированные состояния системы, если поток ожидает пока пройдет нотификация проще понимать что происходит в системе (проще фиксить баги в некоторых случаях к примеру) R>Если выбирать между гарантией неполучения сообщения после вызова Unsubscribe в синхронных сообщениях и асинхронными сообщениями я скорее выбрал бы синхронные.
По сути в шаблоне издатель-подписчик, издателю все равно как подписчик отреагировал на полученное сообщение. Если хочется обязательно получить ответ от подписчиков, то я бы сделал обратную связь с подписчиками (установка флага, счетчик принятых сообщений и т.д.). Рассылку сообщений в другой поток сделать асинхронно.
Хочу поблагодарить всех, кто обратил внимание на мой пост и написал свои замечания/советы.
также хочу представить на общий суд отредактированный код, который, надеюсь, учитывает большинство замечаний/предложений.
Отдельное спасибо хочу выразить пользователю remark за идею копирования списка при добавлении-удалении, правда мне предстоит непростая задача объяснить доходно об этой технологии, не уходя далеко от тематики статьи.
в общем вот, версия 1.0.0.2:
namespace Observer
{
//////////////////////////
// Subscriber
//////////////////////////typedef unsigned __int64 SubscriberId;
class CSubscriber
{
public:
virtual ~CSubscriber(){}
virtual void OnNotification(void* pContext) = 0;
SubscriberId GetSubscriberId() {return (SubscriberId)this;}
};
typedef boost::shared_ptr<CSubscriber> CSubscriberPtr;
//////////////////////////////////////////////////////////////////////
// Dispatcher
///////////////////////////////////class CDispatcher
{
private:
typedef std::vector<CSubscriberPtr> CSubscriberList;
typedef boost::shared_ptr<CSubscriberList> CSubscriberListPtr;
public:
SubscriberId Subscribe(CSubscriberPtr pNewSubscriber)
{
//Declaration of the next shared pointer before ScopeLocker
//prevents release of subscribers from under lock
CSubscriberListPtr pNewSubscriberList(new CSubscriberList());
//Enter to locked section
CScopeLocker ScopeLocker(m_Lock);
if(m_pSubscriberList)
{
//Copy existing subscribers
pNewSubscriberList->assign(m_pSubscriberList->begin(), m_pSubscriberList->end());
}
for(size_t i = 0; i < pNewSubscriberList->size(); ++i)
{
CSubscriberPtr pCurrSubscriber = (*pNewSubscriberList)[i];
if(pCurrSubscriber->GetSubscriberId() == pNewSubscriber->GetSubscriberId())
{
return 0;
}
}
//Add new subscriber to new subscriber list
pNewSubscriberList->push_back(pNewSubscriber);
//Exchange subscriber lists
m_pSubscriberList = pNewSubscriberList;
return pNewSubscriber->GetSubscriberId();
}
bool Unsubscribe(SubscriberId id)
{
//Declaration of the next shared pointers before ScopeLocker
//prevents release of subscribers from under lock
CSubscriberPtr pSubscriberToRelease;
CSubscriberListPtr pNewSubscriberList;
//Enter to locked section
CScopeLocker ScopeLocker(m_Lock);
if(!m_pSubscriberList)
{
//No subscribersreturn false;
}
pNewSubscriberList = CSubscriberListPtr(new CSubscriberList());
for(size_t i = 0; i < m_pSubscriberList->size(); ++i)
{
CSubscriberPtr pCurrSubscriber = (*m_pSubscriberList)[i];
if(pCurrSubscriber->GetSubscriberId() == id)
{
pSubscriberToRelease = pCurrSubscriber;
}
else
{
pNewSubscriberList->push_back(pCurrSubscriber);
}
}
//Exchange subscriber lists
m_pSubscriberList = pNewSubscriberList;
if(!pSubscriberToRelease.get())
{
return false;
}
return false;
}
void SendNotification(void* pContext)
{
CSubscriberListPtr pSubscriberList;
{
CScopeLocker ScopeLocker(m_Lock);
if(!m_pSubscriberList)
{
//No subscribersreturn;
}
//Get shared pointer to an existing list of subscribers
pSubscriberList = m_pSubscriberList;
}
//pSubscriberList pointer to copy of subscribers' listfor(size_t i = 0; i < pSubscriberList->size(); ++i)
{
(*pSubscriberList)[i]->OnNotification(pContext);
}
}
private:
CSubscriberListPtr m_pSubscriberList;
CLock m_Lock;
};
}; //namespace Observer
Здравствуйте, Ryadovoy, Вы писали:
R>Не совсем понял идею родительских потоков (это как?)
Как окна в Виндовс. Каждое окно всегда связано с родительским потоком, любой код, связанный с окном, должен выполняться только из этого потока. Соотв. такой поток естественно сериализует доступ к окну (или к подписчику), в одном потоке нотификация и отписка уже не могут происходить одновременно.
R>Мне кажется не всегда удобно пользоваться асинхронными сообщениями, например если надо у пользователя спросить что-то и в зависимости от ответа продолжить или остановится, с асинхронными сообщениями это будет сделать сложнее. Еще проще для понимания фиксированные состояния системы, если поток ожидает пока пройдет нотификация проще понимать что происходит в системе (проще фиксить баги в некоторых случаях к примеру) R>Если выбирать между гарантией неполучения сообщения после вызова Unsubscribe в синхронных сообщениях и асинхронными сообщениями я скорее выбрал бы синхронные.
Синхронные сообщения безусловно удобнее, но дедлоки это их бич.
Здравствуйте, baily, Вы писали:
B>Есть много разных способов. Например, можно связать с каждым подписчиком класс-защелку. К ней должен иметь доступ как подписчик, так и уведомитель. B>Защелка открывается в методе Subscribe после добавление подписчика. В методе Unsubscribe она закрывается уведомителем под критической секцией. B>Класс подписчик в своем методе OnNotification должен вначале проверять, разрешен ли ему вызов. Если нет, то просто выходить.
Если он будет просто проверять в начале функции, то мы возвращаемся туда, откуда начали, — может работать нотификация уже отписанного объекта. Ровным счётом ничего не решается.
Какие ешё есть разные способы?
B>Защелкой должен владеть подписчик. Можно просто создать обертку над подписчиком ( в принципе и умный указатель есть обертка ), который владеет и подписчиком B>и защелкой. В более сложном варианте можно сделать защелку более сложной, поместив туда еще и событие. При вызове OnNotification, когда защелка открыта, B>можно увеличивать счетчик использования защелки в начале и уменьшать в конце. А событие взводить, когда счетчик станет равным нулю. Тогда на событии можно ожидать, B>когда подписчик закончит обрабатывать свой OnNotification.
Здравствуйте, Ryadovoy, Вы писали:
R>Можно попробовать ожидать завершение текущего уведомления в функции Unsubscribe, но тогда скорей всего будут зависания при отключении. R>Я пока что не нашел подходящее решение с реализацией Unsubscribe, гарантирующей прекращение нотификаций после выхода. R>Если есть какие-то идеи по этому поводу, буду рад услышать.
Ещё один вариант решения — трактовать unsubscribe() не как отписку, а как старт асинхронной отписки, а завершение отписки происходит, когда объект получает вызов release(). Т.е. поток вызывает unsubscribe(), этим от говорит, что он хочет инициировать отписку; но никакой код, которы должен отработать только после завершения отписки, он ещё не вызывает. Потом (или возможно сразу) объекту поступает вызов release(), который говорит о том, что отписка завершена, и теперь уже никаких вызовов объекту точно поступать не будет, теперь можно вызывать код, который должен отработать только после завершения отписки.
Здравствуйте, remark, Вы писали:
R>Здравствуйте, baily, Вы писали:
B>>Есть много разных способов. Например, можно связать с каждым подписчиком класс-защелку. К ней должен иметь доступ как подписчик, так и уведомитель. B>>Защелка открывается в методе Subscribe после добавление подписчика. В методе Unsubscribe она закрывается уведомителем под критической секцией. B>>Класс подписчик в своем методе OnNotification должен вначале проверять, разрешен ли ему вызов. Если нет, то просто выходить.
R>Если он будет просто проверять в начале функции, то мы возвращаемся туда, откуда начали, — может работать нотификация уже отписанного объекта. Ровным счётом ничего не решается.
Я имел ввиду следующее. Надо создать класс-обертку, которая владеет и подписчиком и защелкой.
Она является частью имплементации класса-уведомителя. Обертка тоже имеет метод OnNotification.
Класс-уведомитель хранит массив таких оберток. Когда уведомитель делает рассылку, то он вызывает OnNotification обертки.
Внутри этого метода и проверяется состояние защелки, и только если она открыта, то вызывается OnNotification подписчика.
R>Какие ешё есть разные способы?
Каких то, отличающихся по сути, я не знаю. Везде используется идея защелки. Меняется только ее вид. В предложенном выше случае это просто целочисленный счетчик.
B>>Защелкой должен владеть подписчик. Можно просто создать обертку над подписчиком ( в принципе и умный указатель есть обертка ), который владеет и подписчиком B>>и защелкой. В более сложном варианте можно сделать защелку более сложной, поместив туда еще и событие. При вызове OnNotification, когда защелка открыта, B>>можно увеличивать счетчик использования защелки в начале и уменьшать в конце. А событие взводить, когда счетчик станет равным нулю. Тогда на событии можно ожидать, B>>когда подписчик закончит обрабатывать свой OnNotification.
R>Это потенциальные дедлоки.
Решение рабочее. Дедлоков при аккуратной работе случаться не должно.
Здравствуйте, baily, Вы писали:
R>>Если он будет просто проверять в начале функции, то мы возвращаемся туда, откуда начали, — может работать нотификация уже отписанного объекта. Ровным счётом ничего не решается.
B>Я имел ввиду следующее. Надо создать класс-обертку, которая владеет и подписчиком и защелкой. B>Она является частью имплементации класса-уведомителя. Обертка тоже имеет метод OnNotification. B>Класс-уведомитель хранит массив таких оберток. Когда уведомитель делает рассылку, то он вызывает OnNotification обертки. B>Внутри этого метода и проверяется состояние защелки, и только если она открыта, то вызывается OnNotification подписчика.
Проверяем состояние защелки — она открыта, начинаем выполнение OnNotification подписчика. В это время другой поток отписывает объект. Первый поток продолжает выполнение OnNotification подписчика, когда объект уже отписан. Вернулись туда, откуда начали. Ничего не решено.
Здравствуйте, remark, Вы писали:
R>Проверяем состояние защелки — она открыта, начинаем выполнение OnNotification подписчика. В это время другой поток отписывает объект. Первый поток продолжает выполнение OnNotification подписчика, когда объект уже отписан. Вернулись туда, откуда начали. Ничего не решено.
С тем же успехом можно убрать эту зашёлку. Наличие объекта в списке уже выполняло роль этой защёлки. Но проблема в том, что обе эти проверки происходят только до начала работы метода, в то время как они должны распространять своё действие на всё время работы метода, если мы хотим, что бы он гарантированно не выполнялся после отписки.
Здравствуйте, remark, Вы писали:
R>Здравствуйте, Ryadovoy, Вы писали:
R>>Можно попробовать ожидать завершение текущего уведомления в функции Unsubscribe, но тогда скорей всего будут зависания при отключении. R>>Я пока что не нашел подходящее решение с реализацией Unsubscribe, гарантирующей прекращение нотификаций после выхода. R>>Если есть какие-то идеи по этому поводу, буду рад услышать.
R>Ещё один вариант решения — трактовать unsubscribe() не как отписку, а как старт асинхронной отписки, а завершение отписки происходит, когда объект получает вызов release(). Т.е. поток вызывает unsubscribe(), этим от говорит, что он хочет инициировать отписку; но никакой код, которы должен отработать только после завершения отписки, он ещё не вызывает. Потом (или возможно сразу) объекту поступает вызов release(), который говорит о том, что отписка завершена, и теперь уже никаких вызовов объекту точно поступать не будет, теперь можно вызывать код, который должен отработать только после завершения отписки.
R>
Я лично именно так бы и сделал, если бы сам писал.
gh2>1) корректно разрешите проблему безопасности исключений gh2>2) избавитесь от связи (время жизни диспетчера) == (время, проведенное в критической секции) gh2>3) гарантируете отсутствие утечки ресурса "объект-синхронизации" (сейчас это критическая секция)
И как я понимаю получится подмножество библиотеки boost::signals2. Signals2 удобнее ещё тем что не надо реализовывать какой-то интерфейс для создания подписчиков. Подписчиком может быть функция, метод класса или функтор, можно использовать так же bind, что очень удобно в некоторых случаях.
Я очень много раз встречал отказ использовать исключения просто потому что люди не понимаю, как строит программы с обработкой ошибок на исключениях. На вопрос, в чём преимущество обработки ошибок через коды возврата, обычно некоторое время думают и говорят, мол типа один знакомый программист Вася, говорил что исключения тормозные, ну и как-то они запутывают код. Вы понимаете что такое ответ говорит лишь о том что человек не понимает исключений, не понимает как ими пользоваться, за предыдущее время работы не попытался их понять и научится ими пользоваться. И кстати если это время больше 2-х лет, то не стоит связываться с таким человеком и брать его на работу.
Лишь один раз я слышал действительно мотивированный отказ от построения обработки ошибок на исключениях, и это google code convention. Они отказываются от исключений в основном потому, что на момент написания code guidelines у них уже написано огромное количество кода с обработкой ошибок на кодах возврата. При этом перед этим идёт честное сравнение плюсов и минусов обеих подходов.
Здравствуйте, remark, Вы писали:
R>Здравствуйте, baily, Вы писали:
R>>>Если он будет просто проверять в начале функции, то мы возвращаемся туда, откуда начали, — может работать нотификация уже отписанного объекта. Ровным счётом ничего не решается.
B>>Я имел ввиду следующее. Надо создать класс-обертку, которая владеет и подписчиком и защелкой. B>>Она является частью имплементации класса-уведомителя. Обертка тоже имеет метод OnNotification. B>>Класс-уведомитель хранит массив таких оберток. Когда уведомитель делает рассылку, то он вызывает OnNotification обертки. B>>Внутри этого метода и проверяется состояние защелки, и только если она открыта, то вызывается OnNotification подписчика.
R>Проверяем состояние защелки — она открыта, начинаем выполнение OnNotification подписчика. В это время другой поток отписывает объект. Первый поток продолжает выполнение OnNotification подписчика, когда объект уже отписан. Вернулись туда, откуда начали. Ничего не решено.
R>
Гм! Согласен. Я поторопился. У меня в коде решение, использующее ту же идею, что у вас в этом
Здравствуйте, TarasKo, Вы писали:
TK>Лишь один раз я слышал действительно мотивированный отказ от построения обработки ошибок на исключениях, и это google code convention. Они отказываются от исключений в основном потому, что на момент написания code guidelines у них уже написано огромное количество кода с обработкой ошибок на кодах возврата. При этом перед этим идёт честное сравнение плюсов и минусов обеих подходов.
Android NDK, например, не поддерживает исключения и RTTI. Во что превращается портирование кода в случае, если ты их используешь, думаю объяснять не надо ))
Здравствуйте, COFF, Вы писали:
COF>Здравствуйте, TarasKo, Вы писали:
TK>>Лишь один раз я слышал действительно мотивированный отказ от построения обработки ошибок на исключениях, и это google code convention. Они отказываются от исключений в основном потому, что на момент написания code guidelines у них уже написано огромное количество кода с обработкой ошибок на кодах возврата. При этом перед этим идёт честное сравнение плюсов и минусов обеих подходов.
COF>Android NDK, например, не поддерживает исключения и RTTI. Во что превращается портирование кода в случае, если ты их используешь, думаю объяснять не надо ))
Исключения там не поддерживаются, потому что в Google их не используют. А не используют, потому что coding conventions такие. Иначе бы давно сделали. Это не так уж и сложно, как показывает следующий проект: http://www.crystax.net/android/ndk-r4.php
Здравствуйте, alexeiz, Вы писали:
COF>>Android NDK, например, не поддерживает исключения и RTTI. Во что превращается портирование кода в случае, если ты их используешь, думаю объяснять не надо ))
A>Исключения там не поддерживаются, потому что в Google их не используют. А не используют, потому что coding conventions такие. Иначе бы давно сделали. Это не так уж и сложно, как показывает следующий проект: http://www.crystax.net/android/ndk-r4.php
Это другой вопрос, на самом деле — почему это так? Если ты не гугл, конечно )) Факт, что на настоящий момент код с исключениями значительно менее портабелен, как это ни прискорбно
Здравствуйте, LeonCrew, Вы писали:
LC>Минусы: LC>- дополнительный расход памяти на дополнительные списки (к каким издателям подписан подписчик), для встраиваемых систем может быть критично;
У меня есть идея что при определенном подходе в написании кода, который использует эту схему, такие списки на стороне подписчиков становятся лишними. Однако я пока не доказал даже для себя что это возможно. Также не видел противоположного доказательства.