Страницы

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

суббота, 14 декабря 2019 г.

Реализация Event

#c_sharp #net #события


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


Ответы

Ответ 1



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

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

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