События и многопоточный код
Недавно, просматривая главу книги Framework Design Guidelines, посвященную событиям (event) наткнулся на интересное замечание. Это же замечание про события процитировано и на блоге Brad Abrams Events, Delegate and Multithreading . Суть его в следующем. Предположим, что у нашего класса есть некое событие Clicked, объявленное следующим образом:
Немного теории. Если вы не очень хорошо знаете внутреннюю кухню событий и делегатов, то предварительно рекомендую просмотреть статью Делегаты и события. Как известно, подписка и отписка от события при помощи C#-операторов += и -= приводит к вызову статических методов Delegate.Combine и Delegate.Remove соответственно. Работа этих методов на наследниках от MulticastDelegate приводит к тому, что будет создан новый делегат, с новым списком вызовов (invocation list), а старый будет собран сборщиком мусора (если на него нет ссылок).
Вернемся теперь к интересной ситуации, когда один поток пытается исполнить метод OnSafeRaiseClick, а второй поток отписывает делегаты. После исполнения первым потоком кода
Интересен тот факт, что дополнительных накладных расходов в методе OnSafeRaiseClick по сравнению с OnUnsafeRaiseClick при однопоточном исполнении очень немного.
public event EventHandler Clicked;Если метод генерации события написан в виде:
protected void OnUnsafeRaiseClick()- это приводит к небезопасному поведению в случае многопоточного исполнения, когда один поток пытается сгенерировать событие, а второй в этот момент отписывает последний делегат. При определенном стечении обстоятельств проверка
{
if (Clicked != null) Clicked(this, EventArgs.Empty);
}
Clicked != nullможет пройти успешно (второй поток еще не отписал последний делегат), а попытка выполнить
Clicked(this, EventArgs.Empty)может привести к исключению System.NullReferenceException (если второй поток уже отписал последнее событие). Рекомендуемым способом генерации события будет следующий:
protected void OnSafeRaiseClick()По правде говоря, мне было совсем не очевидно, почему копирование ссылки делает исполнение кода безопасным в многопоточной среде, а поиск в Google конкретно по этому вопросу ничего не дал, поэтому провел свое маленькое исследование.
{
EventHandler handler = Clicked;
if (handler != null)
handler(this, EventArgs.Empty);
}
Немного теории. Если вы не очень хорошо знаете внутреннюю кухню событий и делегатов, то предварительно рекомендую просмотреть статью Делегаты и события. Как известно, подписка и отписка от события при помощи C#-операторов += и -= приводит к вызову статических методов Delegate.Combine и Delegate.Remove соответственно. Работа этих методов на наследниках от MulticastDelegate приводит к тому, что будет создан новый делегат, с новым списком вызовов (invocation list), а старый будет собран сборщиком мусора (если на него нет ссылок).
Вернемся теперь к интересной ситуации, когда один поток пытается исполнить метод OnSafeRaiseClick, а второй поток отписывает делегаты. После исполнения первым потоком кода
EventHandler handler = Clickedмодификация вторым потоком Clicked приведет к созданию нового делегата с новым списком вызовов и событие Clicked будет хранить ссылку на новый делегат или null (если отписали последнее событие), а handler будет хранить ссылку на первоначальный делегат. Далее исполнение первым потоком
if (handler != null)будет вестись над исходным делегатом и проблемы не возникнет.
handler(this, EventArgs.Empty);
Интересен тот факт, что дополнительных накладных расходов в методе OnSafeRaiseClick по сравнению с OnUnsafeRaiseClick при однопоточном исполнении очень немного.
Комментарии
Вот классная вещь http://quickhighlighter.com/code-syntax-highlighter.php
неудобно лишь то, что надо потом смотреть исходник страницы и выковыривать html-код.