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

Так це і є програмування вантажних культів ? Мабуть, так - багато людей повинні робити цей крок, щоб захистити свій код від декількох потоків, коли насправді мені здається, що події вимагають набагато більше уваги, ніж це, перш ніж їх можна використовувати як частину багатопотокової конструкції . Отже, люди, які не сприймають такої додаткової обережності, можуть також ігнорувати цю пораду - це просто не є проблемою для однопотокових програм, а насправді, зважаючи на відсутність volatileу більшості прикладних кодів в Інтернеті, поради можуть бути відсутніми ефект взагалі.

(І чи не набагато простіше просто призначити порожнє delegate { } в декларації члена, щоб ніколи не потрібно було перевірятиnull це?)

Оновлено:Якщо це не було зрозуміло, я зрозумів намір поради - уникати нульового виключення за будь-яких обставин. Моя думка полягає в тому, що цей конкретний нульовий посилання на виняток може виникнути лише в тому випадку, якщо інший потік відхиляється від події, і єдиною причиною цього є забезпечення того, щоб додаткові дзвінки не надходили через цю подію, що очевидно НЕ досягається цією технікою . Ви б приховували умову гонки - краще було б розкрити її! Цей нульовий виняток допомагає виявити зловживання вашим компонентом. Якщо ви хочете, щоб ваш компонент був захищений від зловживань, ви можете наслідувати приклад WPF - зберігайте ідентифікатор потоку у своєму конструкторі, а потім кидайте виняток, якщо інший потік намагається взаємодіяти безпосередньо з вашим компонентом. Або ж реалізуйте справді безпечний для компонентів компонент (завдання не з легких).

Тому я стверджую, що просто виконувати цю ідіому копіювання / перевірки - це культове програмування вантажу, додаючи безлад та шум у ваш код. Щоб реально захиститись від інших ниток, потрібно набагато більше роботи.

Оновлення у відповідь на повідомлення в блозі Еріка Ліпперта:

Тож є головне, що я пропустив у обробниках подій: "Обробники подій повинні бути надійними, якщо вони можуть бути викликані навіть після відмови від підписки", і, очевидно, тому нам потрібно дбати лише про можливість події. делегат буття null. Чи є ця вимога до обробників подій документована де-небудь?

І так: "Є й інші способи вирішити цю проблему; наприклад, ініціалізація обробника на порожню дію, яка ніколи не видаляється. Але проведення нульової перевірки є стандартною схемою".

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

Безумовно, що так (як запропонував Джон Скіт), це просто .NET 1.x порада, яка не вимерла, як це слід було зробити в 2005 році?


4
Це питання під час внутрішньої дискусії виникло; Я вже маю намір вести щоденник на цьому блозі. Мій пост на цю тему тут: Події та перегони
Ерік Ліпперт

3
Стівен Клірі має статтю CodeProject, яка вивчає цю проблему, і він робить висновок про загальну мету: "безпечного для потоків" рішення не існує. В основному, виклик подій повинен переконатися, що делегат не є нульовим, а також до обробника події, щоб мати змогу обробляти виклики після його відміни.
rkagerer

3
@rkagerer - насправді другою проблемою іноді доводиться займатися обробник подій, навіть якщо теми не задіяні. Це може статися, якщо один обробник події скаже іншому оброблювачу скасувати підписку на подію, яка зараз обробляється, але цей 2-й абонент потім отримає подію все-таки (тому що він підписався під час обробки).
Даніель Ервікер

3
Додавання підписки на подію з нульовими абонентами, видалення єдиної підписки на подію, виклик події з нульовими абонентами та виклик події саме з одним абонентом - це набагато швидші операції, ніж додавання / видалення / виклик сценаріїв, що включають інші числа передплатники. Додавання манекена-делегата сповільнює загальний випадок. Справжня проблема C # полягає в тому, що його творці вирішили EventName(arguments)беззастережно викликати делегата події, а не викликати цього лише делегата, якщо він не має значення (не робити нічого, якщо нуль).
supercat

Відповіді:


100

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

Без змінного модифікатора можливо, що зроблена локальна копія застаріла, але це все. Це не спричинитьNullReferenceException .

І так, безумовно, є умова гонки - але завжди буде така. Припустимо, ми просто змінимо код на:

TheEvent(this, EventArgs.Empty);

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

Використання порожнього делегата, безумовно, дозволяє уникнути перевірки недійсності, але не виправляє умову гонки. Це також не гарантує того, що ви завжди "бачите" останнє значення змінної.


4
"Сучасне програмування на Windows" Джо Даффі охоплює аспект питання оптимізації JIT та моделі пам'яті; дивіться code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
Я прийняв це, грунтуючись на коментарі до того, що "стандартні" поради ставляться до C # 2, і я не чую, щоб хтось суперечив цьому. Якщо справді не дорого створювати аргументи події, просто покладіть "= delegate {}" в кінці декларації про події, а потім зателефонуйте безпосередньо своїм подіям так, ніби вони є методами; ніколи не призначайте їм null. (Інші речі, які я взяв із собою щодо того, щоб обробник не викликався після вилучення, це було абсолютно неактуально, і це неможливо забезпечити навіть для одного потокового коду, наприклад, якщо обробник 1 попросить обробник 2 перекреслити, обробник 2 все одно отримає виклик наступний.)
Даніель Ервікер

2
Єдиний проблемний випадок (як завжди) - це структури, де ви не можете гарантувати, що вони будуть інстанціюватися нічим іншим, ніж нульовими значеннями в їхніх членах. Але структури смокчуть.
Даніель Ервікер

1
Про порожній делегат див. Також це питання: stackoverflow.com/questions/170907/… .
Володимир

2
@Tony: Все ще існує умова перегонів між тим, що підписується / відписується, та делегатом, що виконується. Ваш код (лише коротко переглянувши його) зменшує цей стан перегонів, дозволяючи вступити в силу підписки / відписки під час підвищення, але я підозрюю, що в більшості випадків, коли нормальна поведінка недостатньо хороша, це теж не так.
Джон Скіт

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() );

А також усуває локальну копію, оскільки вона зберігається під час виклику методу.


9
Мені подобається синтаксис, але давайте зрозуміти ... він не вирішує проблему з несвіжим обробником, який викликається навіть після того, як він був незареєстрований. Це вирішує лише проблему з нульовим відношенням. Хоча мені подобається синтаксис, я сумніваюся, чи він справді кращий за: публічний захід EventHandler <T> MyEvent = delete {}; ... MyEvent (це, нові MyEventArgs ()); Це також дуже низьке тертя, яке мені подобається за його простоту.
Саймон Гіллбі

@Simon Я бачу різних людей, які говорять про це різні речі. Я перевірив це, і те, що я зробив, вказує на мене, що це вирішує проблему з нульовим обробником. Навіть якщо оригінал Sink відреєстраторів події після обробника!
JP Alioto

да, Cf це питання: stackoverflow.com/questions/192980 / ...
Benjol

1
+1. Я просто написав цей метод сам, почавши думати про безпеку ниток, провів деякі дослідження і натрапив на це питання.
Niels van der Rest

Як це можна зателефонувати з VB.NET? Або "RaiseEvent" вже задовольняє багатопотокові сценарії?

35

"Чому явний-null перевіряє" стандартний шаблон "?"

Я підозрюю, що причиною цього може бути те, що нульова перевірка є більш ефективною.

Якщо ви завжди підписуєте порожній делегат на свої події, коли вони створюються, будуть деякі накладні витрати:

  • Вартість побудови порожнього делегата.
  • Вартість побудови ланцюга делегата для його утримання.
  • Вартість виклику безглуздого делегата щоразу, коли подія піднімається.

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

Я зробив кілька побіжних тестувань продуктивності, щоб побачити вплив підходу «підписатись-порожній-делегат», і ось мої результати:

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 Event, яку я опублікував за день до того, як це питання було задано (!)

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


8
Я думаю, що ви зробите ключовий момент у своєму дописі в блозі: не потрібно турбуватися про наслідки для продуктивності, поки це не вузьке місце. Чому нехай некрасивий спосіб є рекомендованим способом? Якби ми хотіли передчасної оптимізації замість ясності, ми б використовували асемблер - тому моє питання залишається, і я думаю, що ймовірною відповіддю є те, що порада просто передує анонімним делегатам, і людський культура потребує тривалого часу, щоб змінити старі поради, наприклад у знаменитій «розповіді про смажені горщики».
Даніель Ервікер

13
І ваші цифри дуже добре підтверджують суть: накладні витрати зменшуються лише до двох з половиною НАНОСЕКУНДІВ !!! Це було б неможливо визначити майже в додатках із справжньою роботою, але враховуючи, що переважна більшість подій використовується в рамках графічного інтерфейсу, вам доведеться порівнювати це з вартістю перефарбовування частин екрана в Winforms тощо. він стає ще непомітнішим у потопі реальної роботи процесора та очікуванні ресурсів. Ви все одно отримаєте +1 від мене за важку роботу. :)
Даніель Ервікер

1
@DanielEarwicker сказав це правильно, ти перемістив мене як віруючого в публічну подію WrapperDoneHandler OnWrapperDone = (x, y) => {}; модель.
Міккі Перлштайн

1
Можливо, також було б добре провести час Delegate.Combine/ Delegate.Removeпару у випадках, коли подія має нуль, одного або двох абонентів; якщо один раз додавати та вилучати один і той же екземпляр делегата, різниця у витратах між справами буде особливо виражена, оскільки Combineмає швидку поведінку у спеціальному випадку, коли один з аргументів є null(просто поверніть інший), і Removeдуже швидко, коли два аргументи є рівний (просто повернути нуль).
supercat

12

Мені справді сподобалось це читання - ні! Хоча мені це потрібно для роботи з функцією C # під назвою події!

Чому б не виправити це в компіляторі? Я знаю, що є люди з MS, які читають ці пости, тому, будь ласка, не горіть цим!

1 - Нульовий випуск ) Чому б не зробити події. По-перше, порожніми, а не нульовими? Скільки рядків коду було б збережено для нульової перевірки або необхідності вклеїти a = delegate {}на декларацію? Нехай компілятор обробляє порожній випадок, IE нічого не робить! Якщо це все має значення для творця події, він може перевірити наявність. Порожні та робити все, що їм важливо! Інакше всі нульові перевірки / додавання делегата - це хакі навколо проблеми!

Чесно кажучи, мені набридло робити це з кожною подією - ака-код котла!

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, який робить це на етапі після компіляції . :)
Стівен Євріс

4
Блок запитів на підписку / скасування підписки на потоки під час очікування завершення довільного зовнішнього коду набагато гірший, ніж передплатники отримують події після скасування підписки, тим більше, що останню "проблему" можна вирішити легко , просто змусивши обробників подій перевірити прапор, щоб побачити, чи вони все ще зацікавлені в отриманні своєї події, але тупики, що виникли в результаті колишньої конструкції, можуть бути незрозумілими.
supercat

@supercat. Імо, коментар "набагато гірший" - це дуже залежна від програми. Хто б не хотів дуже жорсткого блокування без зайвих прапорів, коли це варіант? Тупик повинен статися лише в тому випадку, якщо потік обробки подій чекає ще на один потік (тобто підписка / підписка), оскільки блокування перевтілення в одну нитку і підписка / скасування підписки в оригінальному обробнику події не буде заблокована. Якщо в рамках обробника подій чекає поперечна нитка, яка буде частиною дизайну, я вважаю за краще переробляти Я надходжу з точки зору сервера програми, яка має передбачувані зразки.
crokusek

2
@crokusek: Аналіз, необхідний для підтвердження того, що система не має тупикового простору, легко, якщо в спрямованому графіку не буде циклів, що з'єднують кожен замок з усіма замками, які можуть знадобитися під час його проведення [відсутність циклів доводить систему без тупика]. Дозволяючи викликати довільний код під час утримання блокування, це створить край графіка "може знадобитися" від цього блокування до будь-якого блокування, який може отримати довільний код (не зовсім кожен замок у системі, але недалеко від нього ). Отже, існування циклів не означає, що станеться тупикова ситуація, але ...
supercat

1
... значно підвищив би рівень аналізу, необхідний для підтвердження того, що він не міг.
supercat

8

З C # 6 і вище, код можна спростити за допомогою нового ?.оператора, як у:

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

Ось документація MSDN.


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);

Тому що це змушує копію довідки. Для отримання додаткової інформації дивіться його розділ Події в книзі.


Можливо, мені не вистачає smth, але Interlocked.CompareExchange викидає NullReferenceException, якщо його перший аргумент є нульовим, саме цього ми хочемо уникати. msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangeне вдалося б, якби якимось чином пройшов нуль ref, але це не те саме, що передано а refдо місця зберігання (наприклад NewMail), яке існує і яке спочатку містить нульове посилання.
supercat

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/…
Еш,

2

Ця практика не стосується виконання певного порядку операцій. Йдеться про те, щоб уникнути нульового посилання виключення.

Міркування людей, які піклуються про нульовий вихідний виняток, а не про перегони, потребують глибокого психологічного дослідження. Я думаю, це має щось спільне з тим, що виправити нульову проблему посилання набагато простіше. Після того, як це буде виправлено, вони вивішують великий код "Виконана місія" на своєму коді та розпакують їх льотний костюм.

Примітка: виправлення умови перегонів, ймовірно, передбачає використання синхронної доріжки прапора, чи повинен обробник працювати


Я не прошу вирішити цю проблему. Мені цікаво, чому є поширена порада розпорошувати додатковий кодовий безлад навколо стрілянини подій, коли це дозволяє лише уникнути нульового винятку, коли існує важко виявити умову гонки, яка все ще буде існувати.
Даніель Ервікер

1
Ну, це було моє бачення. Їх не хвилює стан перегонів. Вони дбають лише про виключення нульової посилання. Я відредагую це у своїй відповіді.
dss539

І моя думка: чому було б доцільно піклуватися про нульовий базовий виняток, а ще не піклуватися про стан гонки?
Даніель Ервікер

4
Правильно написаний обробник подій повинен бути готовий вирішувати той факт, що будь-який конкретний запит на викликання події, обробка якого може перекривати запит на додавання або видалення, може або не може в кінцевому підсумку викликати подію, яку додавали чи видаляли. Причина, яка програмістів не хвилює стан гонки, полягає в тому, що в правильно написаному коді не має значення, хто виграє .
supercat

2
@ dss539: Хоча можна створити рамку події, яка б блокувала запити на підписку, поки після закінчення викликів подій не буде закінчено, такий дизайн унеможливить будь-яку подію (навіть щось на зразок Unloadподії) безпечно скасувати передплату об'єкта на інші події. Неприємний. Краще просто сказати, що запити на підписку на події призведуть до того, що "під кінець трансляції" підписуються на події, і передплатники подій повинні перевірити, коли вони викликаються, чи є щось корисне для них.
supercat

1

Тому я трохи запізнююся на вечірку тут. :)

Щодо використання нульового, а не нульового об'єктного шаблону для представлення подій без підписників, розглянемо цей сценарій. Потрібно викликати подію, але побудова об'єкта (EventArgs) нетривіальна, і в загальному випадку у вашої події немає підписників. Вам було б корисно, якби ви могли оптимізувати свій код, щоб перевірити, чи були у вас підписки взагалі, перш ніж ви доклали зусиль для обробки конструкції аргументів та виклику події.

Зважаючи на це, рішення полягає в тому, щоб сказати, "ну нуль абонентів представлений нулем". Тоді просто виконайте нульову перевірку перед виконанням дорогої операції. Я вважаю, що іншим способом цього було б мати властивість Count типу Delegate, тож дорогу операцію ви виконували б лише, якщо myDelegate.Count> 0. Використання властивості Count - дещо приємний зразок, який вирішує початкову проблему дозволяє оптимізувати, а також має приємне властивість можливості викликати, не викликаючи NullReferenceException.

Майте на увазі, що оскільки делегати є типовими типами, вони можуть бути нульовими. Можливо, просто не було хорошого способу приховувати цей факт під прикриттями та підтримувати лише нульовий шаблон об’єкта для подій, тому альтернатива, можливо, змусила розробників перевіряти як на нуль, так і на нульових абонентів. Це було б навіть гірше, ніж нинішня ситуація.

Примітка. Це чиста спекуляція. Я не пов'язаний з мовами .NET або CLR.


Я припускаю, що ви маєте на увазі "використання порожнього делегата, а не ..." Ви вже можете робити те, що пропонуєте, з ініціалізацією події до порожнього делегата. Тест (MyEvent.GetInvocationList (). Довжина == 1) буде істинним, якщо початковий порожній делегат є єдиним у списку. Ще не потрібно спочатку робити копію. Хоча я думаю, випадок, який ви описуєте, в будь-якому випадку був би надзвичайно рідкісним.
Даніель Ервікер

Я думаю, ми тут плутаємо ідеї делегатів та події. Якщо у мене в класі є подія Foo, тоді, коли зовнішні користувачі викликають MyType.Foo + = / - =, вони насправді викликають методи add_Foo () та delete_Foo (). Однак, коли я посилаюсь на Foo з класу, де він визначений, я фактично посилаюся на базового делегата безпосередньо, а не на методи add_Foo () та delete_Foo (). І при існуванні таких типів, як EventHandlerList, нічого не вимагає, щоб делегат і подія навіть були в одному місці. Це те, що я мав на увазі під моментом своєї відповіді абзацом «Майте на увазі».
Леві

(продовж.) Я визнаю, що це заплутаний дизайн, але альтернатива, можливо, була гіршою. Оскільки врешті-решт все, що у вас є, є делегатом - ви можете посилатися на базового делегата безпосередньо, ви можете отримати його з колекції, ви можете створити його на ходу - це, можливо, технічно було б неможливо підтримати що-небудь, крім "перевірки на нульовий "візерунок.
Леві

Поки ми говоримо про розгортання події, я не можу зрозуміти, чому тут важливі додавання та видалення аксесуарів.
Даніель Ервікер

@Levi: Мені дуже не подобається, як C # поводиться з подіями. Якби я мав своїх барабанщиків, делегату дали б інше ім’я, ніж подія. За межами класу єдиними допустимими операціями над назвою події були б +=і -=. У межах класу допустимі операції також включатимуть виклик (із вбудованою нульовою перевіркою), тестування проти nullчи встановлення на null. Щодо іншого, то доведеться використовувати делегата, ім'я якого буде ім'ям події, з якимсь певним префіксом або суфіксом.
supercat

0

для однопоточних додатків, ви коригуєте це не проблема.

Однак, якщо ви створюєте компонент, який розкриває події, немає гарантії, що споживач вашого компонента не збирається переходити до багатопотокової роботи, і в цьому випадку вам потрібно підготуватися до гіршого.

Використання порожнього делегата вирішує проблему, але також спричиняє враження від продуктивності під час кожного дзвінка на подію і, можливо, може мати наслідки для GC.

Ви маєте рацію, що споживач trie dto скасував підписку, щоб це сталося, але якщо вони пройшли минулу копію, то розгляньте повідомлення, яке вже перебуває в дорозі.

Якщо ви не використовуєте тимчасову змінну і не використовуєте порожній делегат, а хтось скасує підписку, ви отримаєте нульове виняткове посилання, яке є фатальним, тому я думаю, що вартість того варта.


0

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

Чи я це роблю неправильно?


Якщо ви виділите екземпляр класу зі змінним станом (поля, які змінюють свої значення), а потім дозволите декільком потокам отримати доступ до одного і того ж екземпляра одночасно, не використовуючи блокування, щоб захистити ці поля від зміни двох потоків одночасно, ви ймовірно, роблять це неправильно. Якщо всі ваші потоки мають власні окремі екземпляри (нічого не ділиться) або всі ваші об'єкти незмінні (після розподілу значення їх полів ніколи не змінюються), ви, ймовірно, добре.
Даніель Ервікер

Мій загальний підхід - залишити синхронізацію до абонента, за винятком статичних методів. Якщо я телефоную, я синхронізую на цьому більш високому рівні. (за винятком об'єкта, єдиним призначенням якого є, звичайно, обробка синхронізованого доступу. :))
Грег Д

@GregD залежить від того, наскільки складний метод і які дані він використовує. якщо це впливає на внутрішніх членів, і ви вирішите працювати в потоковому / Заданому стані, вам дуже боляче
Міккі Перлштайн

0

Передайте всі свої події на будівництві та залиште їх у спокої. Дизайн класу Delegate, можливо, не може правильно впоратися з будь-яким іншим використанням, як я поясню в заключному пункті цієї публікації.

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

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

Єдиний раз, коли обробник не повинен отримувати повідомлення про те, що подія сталася, це якщо подія насправді не сталася! Тож якщо ви не хочете, щоб обробник отримував сповіщення, припиніть генерувати події (тобто відключіть керування або все, що є відповідальним за виявлення та приведення події в першу чергу).

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


[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, 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

Я не вірю, що питання обмежується типом c # "подія". Знімаючи це обмеження, чому б не винайти колесо трохи і не зробити щось у цьому напрямку?

Безпечно піднімати нитку подій - найкраща практика

  • Можливість підписати / скасувати підписку на будь-яку нитку під час підйому (умова перегону видалено)
  • Оператор перевантажує для + = і - = на рівні класу.
  • Загальний делегат, визначений абонентом

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).
Луаан
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.