События C # и безопасность потоков - PullRequest
228 голосов
/ 24 апреля 2009

UPDATE

Начиная с C # 6, ответ на этот вопрос:

SomeEvent?.Invoke(this, e);

Я часто слышу / читаю следующий совет:

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

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Обновлено . Читая об оптимизации, я подумал, что для этого также может потребоваться изменчивость члена события, но Джон Скит в своем ответе заявляет, что CLR не оптимизирует копию.

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

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

Фактическая последовательность может быть такой:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Суть в том, что OnTheEvent запускается после того, как автор отписался, и все же они просто отписались специально, чтобы избежать этого. Конечно, что действительно необходимо, так это пользовательская реализация событий с соответствующей синхронизацией в методах доступа add и remove. Кроме того, существует проблема возможных взаимоблокировок, если блокировка удерживается во время запуска события.

Так это Программирование грузового культа ? Кажется, так - многие люди должны предпринять этот шаг, чтобы защитить свой код от нескольких потоков, тогда как на самом деле мне кажется, что события требуют гораздо большего внимания, чем это, прежде чем их можно будет использовать как часть многопоточного дизайна. , Следовательно, люди, которые не проявляют такой дополнительной заботы, могут также проигнорировать этот совет - это просто не проблема для однопоточных программ, и фактически, учитывая отсутствие volatile в большинстве примеров кода в Интернете, совет может не иметь никакого эффекта вообще.

(И не намного ли проще просто присвоить пустую delegate { } в декларации члена, чтобы вам никогда не приходилось проверять null в первую очередь?)

Обновлено: В случае, если неясно, я понял намерение совета - избегать исключения нулевой ссылки при любых обстоятельствах. Моя точка зрения заключается в том, что это исключение с нулевой ссылкой может возникнуть только в том случае, если другой поток исключен из события, и единственная причина для этого состоит в том, чтобы гарантировать, что никакие дополнительные вызовы не будут получены через это событие, что явно НЕ достигается этим методом , Вы бы скрывали состояние гонки - было бы лучше раскрыть это! Это нулевое исключение помогает обнаружить злоупотребление вашим компонентом. Если вы хотите, чтобы ваш компонент был защищен от злоупотреблений, вы можете последовать примеру WPF - сохранить идентификатор потока в конструкторе и затем выдать исключение, если другой поток пытается напрямую взаимодействовать с вашим компонентом. Или же реализовать действительно потокобезопасный компонент (задача не из легких).

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

Обновление в ответ на сообщения Эрика Липперта в блоге:

Итак, есть одна важная вещь, которую я упустил в обработчиках событий: «обработчики событий должны быть устойчивыми перед вызовом даже после того, как событие было отписано», и, следовательно, поэтому нам нужно заботиться только о возможности делегата мероприятия null. Задокументировано ли это требование к обработчикам событий?

И так: «Существуют и другие способы решения этой проблемы; например, инициализация обработчика для получения пустого действия, которое никогда не удаляется. Но выполнение нулевой проверки является стандартным шаблоном».

Итак, оставшийся фрагмент моего вопроса: почему явно-нулевая проверка "стандартного шаблона"? Альтернатива, назначающая пустой делегат, требует добавления только = delegate {} к объявление события, и это устраняет эти маленькие груды вонючей церемонии из каждого места, где происходит событие. Было бы легко убедиться, что пустой делегат дешевый для создания экземпляра. Или я все еще что-то упускаю?

Конечно, должно быть, (как предположил Джон Скит) это всего лишь совет .NET 1.x, который не исчез, как это должно было быть в 2005 году?

Ответы [ 15 ]

0 голосов
/ 22 октября 2013

Я не верю, что вопрос ограничен типом события c #. Сняв это ограничение, почему бы не заново изобрести колесо и сделать что-то подобное?

Безопасное повышение потока событий - наилучшая практика

  • Возможность подписки / отписки от любой темы, находящейся в рейсе (гонка) условие снято)
  • Операторские перегрузки для + = и - = на уровне класса.
  • Общий определяемый вызывающим абонентом делегат
0 голосов
/ 26 апреля 2013

Пожалуйста, посмотрите здесь: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Это правильное решение, и его всегда следует использовать вместо всех других обходных путей.

«Вы можете убедиться, что во внутреннем списке вызовов всегда есть хотя бы один член, инициализировав его анонимным методом бездействия. Поскольку ни одна внешняя сторона не может иметь ссылку на анонимный метод, никакая внешняя сторона не может удалить метод, поэтому делегат никогда не будет нулевым ». - Программирование компонентов .NET, 2-е издание, автор Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  
0 голосов
/ 27 мая 2009

Связывай все свои события на стройке и оставь их в покое. Дизайн класса Delegate не может правильно обрабатывать любое другое использование, как я объясню в последнем абзаце этого поста.

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

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

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

Честно говоря, я думаю, что класс Delegate не спасен. Слияние / переход к MulticastDelegate было огромной ошибкой, потому что оно фактически изменило (полезное) определение события с того, что происходит в один момент времени, на то, что происходит за промежуток времени. Такое изменение требует механизма синхронизации, который может логически свернуть его обратно в одно мгновение, но MulticastDelegate не имеет такого механизма. Синхронизация должна охватывать весь промежуток времени или момент, когда происходит событие, поэтому, когда приложение принимает синхронизированное решение начать обработку события, оно завершает его обработку полностью (транзакционно). С черным ящиком, который является гибридным классом MulticastDelegate / Delegate, это практически невозможно, поэтому придерживаются использования одного подписчика и / или реализуют свой собственный тип MulticastDelegate, который имеет дескриптор синхронизации, который может быть удален, пока используется / модифицируется цепочка обработчиков . Я рекомендую это, потому что альтернативой может быть избыточная реализация синхронизации / целостности транзакций во всех ваших обработчиках, что было бы нелепо / излишне сложно.

0 голосов
/ 24 апреля 2009

Я никогда не считал, что это большая проблема, потому что я, как правило, защищаю только от такого рода потенциальной проблемы с потоками в статических методах (и т. Д.) На моих повторно используемых компонентах и ​​не создаю статические события.

Я делаю это неправильно?

0 голосов
/ 24 апреля 2009

для однопоточных приложений, вы исправили, это не проблема.

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

Использование пустого делегата решает проблему, но также вызывает снижение производительности при каждом вызове события и может иметь последствия для GC.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...