Страницы

Поиск по вопросам

понедельник, 22 октября 2018 г.

Реализация Event

При помощи ILSpy, гугла и десятка-другого нецензурных слов, мне удалось точно воспроизвести реализацию методов Add и Remove стандартного event
В итоге код события выглядит так, во всяком случае после компиляции в Release, IL-код получается эквивалентным на 100%. В Debug - он естественно отличается, но незначительно, а именно в части проверки условия цикла.
//field-like объявление события public event Action SampleEvent1;
//итоги декомпиляции private Action eventDelegate; public event Action SampleEvent2 { add { Action current = eventDelegate, Action comparer; do { comparer = current; Action combine = comparer + value; current = Interlocked.CompareExchange(ref eventDelegate, combine, comparer); } while (!object.ReferenceEquals(current, comparer)); } remove { Action current = eventDelegate, Action comparer; do { comparer = current; Action combine = comparer - value; current = Interlocked.CompareExchange(ref eventDelegate, combine, comparer); } while (!object.ReferenceEquals(current, comparer)); } }
А теперь вопрос знатокам: я понимаю что это все нужно для поддержки многопоточности и что использование lock может приводить к взаимным блокировкам, но я не до конца понимаю как и, самое главное, почему работает этот вариант.


Ответ

Смотрите.
Подписка на/отписка от событий должны быть атомарными, чтобы не было возможно, что кто-то подписался, а событие не приходит. Старые версии делали блокировку:
private EventHandler _myEvent; public event EventHandler MyEvent { add { lock (this) _myEvent += value; } remove { lock (this) _myEvent -= value; } }
Недостаток этого метода — блокировка требует объекта, а какой объект брать? Можно взять «невидимый» объект, но этот объект должен быть тогда как-то специфицирован в стандарте и доступен для использования (например, если мы хотим под той же блокировкой прочитать значение делегата), что не так уж и хорошо, поскольку предписывает деталь имплементации. Поэтому используется this
Но this в свою очередь ведёт к другой проблеме: он может быть заблокирован снаружи, кем угодно! Поэтому было решено отказаться от этой идеи, и перейти к неблокирующему (lock-free) алгоритму, который не требует блокировочного объекта, и вдобавок ко всему просто быстрее и эффективнее.

Как это работает? А вот как. Переименую немного переменные:
Action eventDelegate;
public void AddSampleEvent1(Action value) { Action current = eventDelegate; Action noncombined; do { noncombined = current; Action combined = (Action)Delegate.Combine(noncombined, value); current = Interlocked.CompareExchange(ref eventDelegate, combined, noncombined); } while (current != noncombined); }
Если посмотреть, что делает Interlocked.CompareExchange, это можно переписать для ясности так:
Action eventDelegate;
public void AddSampleEvent1(Action value) { Action current = eventDelegate; Action noncombined; do { noncombined = current; Action combined = (Action)Delegate.Combine(noncombined, value); atomic // фиктивное ключевое слово { if (noncombined == eventDelegate) eventDelegate = combined; current = eventDelegate; } } while (current != noncombined); }
Что происходит? В current на начало итерации цикла будет значение eventDelegate. Мы запоминаем его во временную переменную noncombined, и добавляем value, получая делегат combined. Теперь мы пытаемся записать результат назад. Если в этой точке наш делегат никто не успел поменять из другого потока (а так скорее всего и будет), то Interlocked.CompareExchange завершится успешно, запишет делегат на место, и в current будет старое значение делегата. Это завершит цикл, проверка current != noncombined даст false
Если же пока мы пытались комбинировать, другой поток изменил eventDelegate, то проверка условия в Interlocked.CompareExchange завершится неуспешно. В этом случае в eventDelegate нельзя ничего писать, ведь мы потеряем изменённое значение! Тогда мы просто записываем это новое значение в current и уходим на следующую итерацию (проверка current != noncombined даст true). На следующей итерации мы сделаем то же самое: с текущим значением eventDelegate попробуем скомбинировать новый делегат, и записать на место, проверяя при этом, никто ли не поменял тем временем eventDelegate снова. Это по идее типичная неблокирующая техника, я видел много подобного кода в неблокирующих алгоритмах.

Комментариев нет:

Отправить комментарий