События C # и безопасность потоков

237

ОБНОВИТЬ

Начиная с 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аксессорах. Кроме того, существует проблема возможных взаимоблокировок, если блокировка удерживается во время срабатывания события.

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

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

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

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

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

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

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

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

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

Дэниел Уорвикер
источник
4
Этот вопрос возник во внутренней дискуссии некоторое время назад; Я собирался опубликовать это в блоге уже некоторое время. Мой пост на эту тему здесь: События и гонки
Эрик Липперт
3
У Стивена Клири есть статья CodeProject, в которой рассматривается эта проблема, и он приходит к выводу, что универсального «поточно-ориентированного» решения не существует. В основном это зависит от вызывающего события, чтобы гарантировать, что делегат не является нулевым, и до обработчика события, чтобы иметь возможность обрабатывать вызов, вызванный после того, как он был отписан.
rkagerer
3
@rkagerer - фактически вторая проблема иногда должна решаться обработчиком событий, даже если потоки не задействованы. Может произойти, если один обработчик события скажет другому обработчику отменить подписку на событие, которое обрабатывается в данный момент, но этот второй подписчик получит событие в любом случае (потому что оно отписалось во время обработки).
Дэниел Эрвикер
3
Добавление подписки на событие с нулевыми подписчиками, удаление единственной подписки на событие, вызов события с нулевым подписчиком и запуск события с ровно одним подписчиком - все это гораздо более быстрые операции, чем сценарии добавления / удаления / вызова с другими номерами абонентов. Добавление фиктивного делегата замедляет общий случай. Реальная проблема с C # заключается в том, что его создатели решили EventName(arguments)безоговорочно вызывать делегат события, а не вызывать его только в случае ненулевого значения (ничего не делать в случае нулевого значения).
суперкат

Ответы:

100

JIT не разрешено выполнять оптимизацию, о которой вы говорите в первой части, из-за условия. Я знаю, что это было поднято как призрак некоторое время назад, но это не верно. (Я проверял это либо с Джо Даффи, либо с Вэнсом Моррисоном некоторое время назад; я не могу вспомнить, какой именно.)

Без модификатора volatile возможно, что локальная копия будет устаревшей, но это все. Это не вызовет NullReferenceException.

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

TheEvent(this, EventArgs.Empty);

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

Использование пустого делегата, безусловно, позволяет избежать проверки недействительности, но не устраняет условия гонки. Это также не гарантирует, что вы всегда «видите» последнее значение переменной.

Джон Скит
источник
4
«Параллельное программирование в Windows» Джо Даффи охватывает аспект оптимизации JIT и модели памяти; см. code.logos.com/blog/2008/11/events_and_threads_part_4.html
Брэдли Грейнджер,
2
Я принял это, основываясь на комментарии о том, что «стандартный» совет был до C # 2, и я не слышал, чтобы кто-то противоречил этому. Если создание ваших аргументов событий действительно дорого, просто поместите '= делегат {}' в конце объявления вашего события, а затем вызовите ваши события напрямую, как если бы они были методами; никогда не назначайте им ноль. (Другая вещь, которую я привел, чтобы гарантировать, что обработчик не вызывается после исключения из списка, это было неуместно, и это невозможно обеспечить даже для однопоточного кода, например, если обработчик 1 запрашивает обработчик 2 для исключения из списка, обработчик 2 все равно будет вызываться далее.)
Дэниел Эрвикер
2
Единственный проблемный случай (как всегда) - это структуры, где вы не можете гарантировать, что они будут созданы с чем-либо, кроме нулевых значений в своих членах. Но структуры сосут.
Дэниел Эрвикер
1
О пустом делегате см. Также этот вопрос: stackoverflow.com/questions/170907/… .
Владимир
2
@ Тони: По-прежнему существует принципиальное условие гонки между чем-то подписывающимся / отписавшимся и выполняемым делегатом. Ваш код (только что кратко просмотрев его) уменьшает это состояние гонки, позволяя подписке / отмене подписки вступать в силу во время ее поднятия, но я подозреваю, что в большинстве случаев, когда нормальное поведение недостаточно хорошо, это тоже не так.
Джон Скит
52

Я вижу, что многие люди идут к расширению метода сделать это ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Это дает вам лучший синтаксис для поднятия события ...

MyEvent.Raise( this, new MyEventArgs() );

А также устраняет локальную копию, поскольку она захватывается во время вызова метода.

JP Alioto
источник
9
Мне нравится синтаксис, но давайте будем ясны ... он не решает проблему с вызовом устаревшего обработчика даже после того, как он был незарегистрирован. Это только решает проблему нулевого разыменования. Хотя мне нравится синтаксис, я спрашиваю, действительно ли он лучше, чем: public event EventHandler <T> MyEvent = delete {}; ... MyEvent (это, новый MyEventArgs ()); Это также решение с очень низким трением, которое мне нравится за его простоту.
Саймон Гилби
@ Симон Я вижу, как разные люди говорят об этом по-разному. Я проверил это, и то, что я сделал, указывает мне, что это действительно обрабатывает проблему нулевого обработчика. Даже если исходный приемник отменяет регистрацию в событии после проверки обработчика! = Null, событие все равно возникает и исключение не выдается.
JP Alioto
да, см. этот вопрос: stackoverflow.com/questions/192980/…
Бенджол
1
+1. Я просто написал этот метод сам, начав думать о безопасности потоков, провел некоторое исследование и наткнулся на этот вопрос.
Нильс ван дер Рест
Как это можно вызвать из VB.NET? Или RaiseEvent уже обслуживает многопоточные сценарии?
35

"Почему явно-нулевая проверка 'стандартного шаблона'?"

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

Если вы всегда подписываете пустой делегат на ваши события при их создании, будут некоторые накладные расходы:

  • Стоимость построения пустого делегата.
  • Стоимость построения цепочки делегатов для ее содержания.
  • Стоимость вызова бессмысленного делегата каждый раз, когда поднимается событие.

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

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

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

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

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

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

Даниэль Фортунов
источник
8
Я думаю, что вы делаете ключевой момент в своем блоге: нет необходимости беспокоиться о влиянии на производительность, пока это не станет узким местом. Почему уродливый путь рекомендуется? Если бы мы хотели преждевременной оптимизации вместо ясности, мы бы использовали ассемблер - поэтому мой вопрос остается, и я думаю, что вероятный ответ заключается в том, что совет просто предшествует анонимным делегатам, и человеческой культуре требуется много времени, чтобы изменить старые советы, например в известной "истории жаркого в горшочке".
Даниэль Эрвикер
13
И ваши цифры очень хорошо подтверждают эту точку зрения: накладные расходы сводятся к двум с половиной НС (!!!) за каждое событие (пре-инициация против классического-ноль). Это было бы невозможно обнаружить почти в приложениях с реальной работой, но, учитывая, что подавляющее большинство событий используется в средах с графическим интерфейсом, вам придется сравнить это со стоимостью перерисовки частей экрана в Winforms и т. Д., Поэтому это становится еще более невидимым в потоке реальной работы процессора и ожидания ресурсов. В любом случае, вы получаете +1 от меня за тяжелую работу. :)
Даниэль Эрвикер
1
@DanielEarwicker правильно сказал, что вы подтолкнули меня стать сторонником публичного мероприятия WrapperDoneHandler OnWrapperDone = (x, y) => {}; модель.
Микки Перлштейн,
1
Также может быть полезно рассчитать Delegate.Combine/ Delegate.Removeпару в случаях, когда у события есть ноль, один или два подписчика; если один раз добавлять и удалять один и тот же экземпляр делегата, различия в затратах между случаями будут особенно заметны, поскольку он Combineимеет быстрое поведение в особом случае, когда один из аргументов null(просто возвращает другой), и Removeочень быстро, когда два аргумента равно (просто вернуть ноль).
суперкат
12

Мне действительно понравилось это чтение - нет! Хотя мне это нужно для работы с функцией C #, которая называется events!

Почему бы не исправить это в компиляторе? Я знаю, что есть люди из MS, которые читают эти посты, так что, пожалуйста, не расстраивайтесь

1 - Нулевая проблема ) Почему бы вообще не сделать события пустыми вместо нулевых? Сколько строк кода будет сохранено для проверки на ноль или необходимости вставлять = delegate {}в объявление? Пусть компилятор обрабатывает пустой случай, IE ничего не делает! Если все это имеет значение для создателя события, они могут проверить .Empty и сделать все, что им захочется! В противном случае все пустые проверки / добавления делегатов - это хаки вокруг проблемы!

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

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - проблема состояния гонки ) Я читаю сообщение Эрика в блоге, я согласен, что H (обработчик) должен обрабатывать, когда он разыменовывает себя, но нельзя ли сделать событие неизменным / безопасным для потока? IE, установите флаг блокировки при его создании, чтобы при каждом вызове он блокировал все подписки и отмены подписки на него во время его выполнения?

Вывод ,

Разве современные языки не должны решать такие проблемы для нас?

Чак Сэвидж
источник
Согласитесь, в компиляторе должна быть лучшая поддержка. До этого я создавал аспект PostSharp, который делает это на этапе после компиляции . :)
Steven Jeuris
4
Блокирование запросов подписки / отмены подписки потока при ожидании завершения произвольного внешнего кода гораздо хуже, чем получение подписчиками событий после отмены подписки, тем более что последнюю «проблему» можно легко решить , просто установив для обработчиков событий флажок, чтобы выяснить, они по-прежнему заинтересованы в получении своего события, но тупики, возникающие из-за прежнего дизайна, могут быть неразрешимыми.
суперкат
@supercat. По моему мнению, «гораздо худший» комментарий зависит от приложения. Кто не хотел бы очень строгую блокировку без дополнительных флагов, когда это возможно? Взаимная блокировка должна возникать только в том случае, если поток обработки событий ожидает еще одного потока (который подписывается / отписывается), так как блокировки являются входящими в тот же поток, и подписка / отмена подписки в исходном обработчике событий не будет заблокирована. Если в обработчике событий есть ожидание кросс-потока, которое будет частью дизайна, я бы предпочел переделать. Я пришел с точки зрения серверного приложения, которое имеет предсказуемые шаблоны.
crokusek
2
@crokusek: Анализ, необходимый для доказательства того, что система свободна от тупика, прост, если в ориентированном графе не будет циклов, соединяющих каждый замок со всеми блокировками, которые могут понадобиться, пока он удерживается [отсутствие циклов доказывает систему взаимоблокировки свободной]. Разрешение произвольного кода вызываться, пока блокировка удерживается, создаст грань в графе «возможно, понадобится» от этой блокировки до любой блокировки, которую может получить произвольный код (не совсем каждая блокировка в системе, но недалеко от нее). ). Последующее существование циклов не означает, что возникнет тупик, но ...
суперкат
1
... значительно повысит уровень анализа, необходимый для доказательства того, что это невозможно.
суперкат
8

С C # 6 и выше, код может быть упрощен с помощью ?.оператора new, как в:

TheEvent?.Invoke(this, EventArgs.Empty);

Вот документация MSDN.

Phil1970
источник
5

Согласно Джеффри Рихтеру в книге CLR через C # , правильный метод:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

Потому что это заставляет справочную копию. Для получения дополнительной информации см. Его раздел «Событие» в книге.

Аликс
источник
Может быть, мне чего-то не хватает, но Interlocked.CompareExchange выдает исключение NullReferenceException, если его первый аргумент равен нулю, чего мы и хотим избежать. msdn.microsoft.com/en-us/library/bb297966.aspx
Книганапольке
1
Interlocked.CompareExchangeпотерпит неудачу, если ему каким-то образом будет передано значение null ref, но это не то же самое, что передать a refв место хранения (например NewMail), которое существует и которое изначально содержит нулевую ссылку.
Суперкат
4

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

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

В настоящее время я в основном работаю с Mono для Android, и Android, похоже, не нравится, когда вы пытаетесь обновить представление после того, как его действие было отправлено в фоновый режим.

ясень
источник
На самом деле, я вижу, что кто-то другой использует очень похожий шаблон: stackoverflow.com/questions/3668953/…
Ash
2

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

Рассуждения людей, которым небезразлично нулевое эталонное исключение, а не состояние гонки, потребуют глубоких психологических исследований. Я думаю, что это как-то связано с тем, что исправить проблему нулевых ссылок гораздо проще. Как только это будет исправлено, они вешают большой код «Миссия выполнена» на свой код и расстегивают молнию на своем летном костюме.

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

dss539
источник
Я не прошу решения этой проблемы. Мне интересно, почему существует широко распространенный совет разбрызгивать дополнительный кодовый беспорядок вокруг запуска событий, когда он избегает нулевого исключения, когда существует трудно обнаруживаемое состояние гонки, которое все еще будет существовать.
Даниэль Уорвикер
1
Ну, это была моя точка зрения. Они не заботятся о состоянии гонки. Их волнует только исключение нулевой ссылки. Я отредактирую это в своем ответе.
dss539
И моя точка зрения такова: почему имеет смысл заботиться об исключении нулевой ссылки и все же не заботиться о состоянии гонки?
Даниэль Уорвикер
4
Правильно написанный обработчик событий должен быть подготовлен для обработки того факта, что любой конкретный запрос на инициирование события, обработка которого может перекрывать запрос на добавление или удаление его, может или не может в конечном итоге вызвать событие, которое было добавлено или удалено. Причина, по которой программисты не заботятся о состоянии гонки, заключается в том, что в правильно написанном коде не имеет значения, кто победит .
суперкат
2
@ dss539: Хотя можно разработать структуру событий, которая будет блокировать запросы на отмену подписки, до тех пор, пока не завершатся отложенные вызовы событий, такой дизайн сделает невозможным для любого события (даже чего-то подобного Unloadсобытию) безопасную отмену подписок объекта на другие события. Насти. Лучше просто сказать, что запросы на отмену подписки на события приведут к отмене подписки на события «в конце концов», и что подписчики событий должны проверять, когда они вызываются, есть ли что-нибудь полезное для них.
суперкат
1

Так что я немного опоздал на вечеринку здесь. :)

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

Учитывая это, решение состоит в том, чтобы сказать: «Ну, нулевые подписчики представлены нулем». Затем просто выполните нулевую проверку перед выполнением дорогостоящей операции. Я предполагаю, что другим способом сделать это было бы иметь свойство Count для типа Delegate, поэтому вы выполняете дорогостоящую операцию, только если myDelegate.Count> 0. Использование свойства Count - довольно приятный шаблон, который решает исходную проблему разрешить оптимизацию, и он также обладает хорошим свойством возможности вызываться, не вызывая исключение NullReferenceException.

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

Примечание: это чистое предположение. Я не связан с языками .NET или CLR.

Леви
источник
Я предполагаю, что вы имеете в виду «использование пустого делегата, а не ...». Вы уже можете делать то, что предлагаете, с событием, инициализированным пустым делегатом. Тест (MyEvent.GetInvocationList (). Length == 1) будет истинным, если исходный пустой делегат является единственной вещью в списке. Там все еще не будет необходимости делать копию в первую очередь. Хотя я думаю, что описанный вами случай в любом случае будет крайне редким.
Дэниел Эрвикер
Я думаю, что мы объединяем идеи делегатов и событий здесь. Если у меня в классе есть событие Foo, то когда внешние пользователи вызывают MyType.Foo + = / - =, они фактически вызывают методы add_Foo () и remove_Foo (). Однако, когда я ссылаюсь на Foo из класса, в котором он определен, я на самом деле ссылаюсь на базовый делегат напрямую, а не на методы add_Foo () и remove_Foo (). А с существованием таких типов, как EventHandlerList, нет ничего обязательного в том, что делегат и событие даже находятся в одном месте. Это то, что я имел в виду под пунктом «Имейте в виду» в своем ответе.
Леви
(продолжение) Я признаю, что это запутанный дизайн, но альтернатива могла быть и хуже. Поскольку, в конце концов, все, что у вас есть, - это делегат - вы можете ссылаться на базовый делегат напрямую, вы можете получить его из коллекции, вы можете создать его на лету - технически невозможно поддерживать что-либо, кроме «проверки для» нулевой "шаблон.
Леви
Поскольку мы говорим о запуске события, я не могу понять, почему методы добавления / удаления здесь важны.
Дэниел Эрвикер
@Levi: мне очень не нравится, как C # обрабатывает события. Если бы у меня были мои барабанщики, делегату дали бы имя, отличное от мероприятия. За пределами класса единственными допустимыми операциями с именем события будут +=и -=. Внутри класса допустимые операции также могут включать в себя вызов (со встроенной проверкой нуля), тестирование nullили настройку на null. Для чего-то еще нужно будет использовать делегата, именем которого будет имя события с некоторым конкретным префиксом или суффиксом.
суперкат
0

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

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

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

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

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

Джейсон Койн
источник
0

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

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

Грег Д
источник
Если вы выделяете экземпляр класса с изменяемым состоянием (поля, которые изменяют свои значения), а затем позволяете нескольким потокам обращаться к одному и тому же экземпляру одновременно, не используя блокировку для защиты этих полей от изменения двумя потоками одновременно, вы вероятно, делает это неправильно. Если все ваши потоки имеют свои отдельные экземпляры (ничего не разделяющие) или все ваши объекты неизменны (после выделения значения их полей никогда не меняются), то вы, вероятно, в порядке.
Даниэль Уорвикер
Мой общий подход - оставить синхронизацию до вызывающей стороны, за исключением статических методов. Если я звоню, то синхронизируюсь на этом более высоком уровне. (за исключением объекта, единственной целью которого является обработка синхронизированного доступа, конечно. :))
Грег Д.
@GregD, это зависит от того, насколько сложен метод и какие данные он использует. если это влияет на внутренних участников, и вы решили работать в многопоточном / заданном состоянии, вам будет очень больно
Микки Перлштейн,
0

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

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

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

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

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

Triynko
источник
[1] Нет полезного обработчика событий, который происходит в «один момент времени». Все операции имеют временной интервал. Любой отдельный обработчик может иметь нетривиальную последовательность шагов для выполнения. Поддержка списка обработчиков ничего не меняет.
Дэниел Эрвикер
[2] Удержание замка во время события - это полное безумие. Это неизбежно ведет в тупик. Источник снимает блокировку A, запускает событие, раковина вынимает блокировку B, теперь удерживаются две блокировки. Что если какая-то операция в другом потоке приведет к снятию блокировок в обратном порядке? Как можно исключить такие смертоносные комбинации, когда ответственность за блокировку разделена между отдельно разработанными / протестированными компонентами (в этом весь смысл событий)?
Дэниел Эрвикер
[3] Ни одна из этих проблем никоим образом не уменьшает всепроникающую полезность обычных многоадресных делегатов / событий в однопоточном составе компонентов, особенно в средах с графическим интерфейсом. Этот вариант использования охватывает подавляющее большинство случаев использования событий. Использование событий свободным потоком имеет сомнительную ценность; это никоим образом не лишает законной силы их дизайн или их очевидную полезность в тех контекстах, где они имеют смысл.
Дэниел Эрвикер
[4] Темы + синхронные события - это по сути красная сельдь. Асинхронное общение в очереди - путь.
Дэниел Эрвикер
[1] Я не имел в виду измеренное время ... Я говорил об атомарных операциях, которые логически происходят мгновенно ... и под этим я имею в виду, что ничто другое, касающееся тех же ресурсов, которые они используют, не может измениться во время события потому что он сериализован с замком.
Трийнко
0

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

«Вы можете убедиться, что во внутреннем списке вызовов всегда есть хотя бы один член, инициализировав его анонимным методом« ничего не делать ». Поскольку ни одна внешняя сторона не может иметь ссылку на анонимный метод, никакая внешняя сторона не может удалить метод, поэтому делегат никогда не будет нулевым »- Программирование .NET Components, 2nd Edition, автор 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);  
}  
Eli
источник
0

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

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

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

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

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

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

И использование это:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Тест

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

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

В конструкторе Bar(объект слушателя) я подписываюсь SomeEvent(что реализовано, как показано выше) и отписываюсь в Dispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

Также у меня есть пара потоков, которые вызывают событие в цикле.

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

Если бы были условия гонки, я бы увидел сообщение в консоли, но оно пустое. Но если я использую события clr как обычно, я вижу, что он полон предупреждающих сообщений. Итак, я могу заключить, что можно реализовать потокобезопасные события в c #.

Что вы думаете?

Тони
источник
Выглядит достаточно хорошо для меня. Хотя я думаю, что (теоретически) это disposed = trueможет произойти раньше foo.SomeEvent -= Handlerв вашем тестовом приложении, что приведет к ложному срабатыванию. Но кроме этого есть несколько вещей, которые вы можете изменить. Вы действительно хотите использовать try ... finallyдля замков - это поможет вам сделать это не только потокобезопасным, но и безопасным для прерывания. Не говоря уже о том, что вы могли бы избавиться от этого глупого try catch. И вы не проверяете делегата, переданного в Add/ Remove- это может быть null(вы должны сразу бросить в Add/ Remove).
Луаан