При помощи 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 снова. Это по идее типичная неблокирующая техника, я видел много подобного кода в неблокирующих алгоритмах.
Комментариев нет:
Отправить комментарий