Friday, August 22, 2014

Шаблоны слабых событий в языке C#

Сегодня мы с вами поговорим об одной из сложный тем в языке C#, а именно  шаблоне слабых событий (Weak Event Patterns). Когда-то я уже рассматривал использование слабых событий в языке C# в теме "Слабые события в C#". В отличие от предыдущей темы, мы поднимемся на уровень выше. Если вы не боитесь глубоко зарываться в исходный код, чтобы докопаться до истины, – материал должен быть вполне интересным. 
Для начала немного теории о понятии шаблонов слабых событий и их использовании. В WPF для решения проблемы с утечкой памяти при использовании событий представляется шаблон, который можно использовать путем предоставления выделенного класса диспетчера для конкретных событий и реализации интерфейса прослушивателей для данного события. Этот шаблон разработки называется шаблоном слабых событий, который в WPF представлен базовым классом WeakEventManager, являющимся типизированной реализацией WeakEventManager<>. Но суть заключается в подходе, который этот шаблон реализует. Например, в библиотеке Prism существует своя реализация шаблона слабых событий, и представлена она с помощью класса WeakEventHandlerManager. Также существует множество других самописных реализаций, именуемых "велосипедами".
Основная причина, которая сподвигла меня на написание этой статьи, энтузиазм разбирать сложные вещи с целью докопаться до самой сути. Это очень увлекательный процесс, и к тому же, неплохо повышает скилл. Учитывая тот факт, что чем больше ты знаешь, тем сложнее найти ментора, который бы разбирался в теме настолько хорошо, как можешь это сделать ты, ничто не может научить лучше, чем методы собственных проб и ошибок. Но это лишь одна из причин возникновения данной темы. Вторая причина заключается в том, что за последних три месяца я уже третий раз вижу очередной "велосипед" для данного паттерна. На мой резонный вопрос, чем не угодили стандартные, реализованные в .NET Framework или в Prism, в ответ обычно следуют невнятные аргументы. У разработчиков уровня Senior/Team Lead аргументация есть хотя бы в плане реализации, в основном касательно быстродействия, но большинство понятия не имеет, как это реализовано, и никогда этим не оперирует. Выглядит диалог следующим образом: (Dev1 – разработчик 1, Dev2 – разработчик 2).
Dev1: Мы вот написали свой кастомный класс WeakEventHandler для работы слабыми событиями.
Dev2: А чем вас не устроил старый класс WeakEventHandler, который идет в поставке?
Dev1: Мы где-то читали, что он очень плохо сделан.
Dev2: А в чем заключается плохая сторона написания этого класса? Чем не вариант использовать реализацию WeakEventHandlerManager, которая доступна в Prism?
Dev1: Все такие классы реализованы одинаково по смыслу опрашивания через некоторое время, живы ссылки или нет.
Dev2: Это сложно отрицать, так как реализация данных шаблонов слабых событий реализована подобным образом. Но это не отменяет того факта, что кастомная реализация сделана вами лучше, чем реализация, идущая в поставке в готовых продуктах.
Это классический отрывок беседы с разработчиком высокого уровня, у которого есть аргументация его действий. Но очень мало разработчиков действительно проверяют быстродействые своих творений, в сравнении с реализациями, которые зачастую входят в поставку.
Давайте отбросим в сторону разные предположения и перейдем непосредственно к фактам и самой реализации паттерна слабых событий, ссылки на которые даны выше. Первым мы рассмотрим WeakEventManager, так как он по реализации посложнее, чем тот же самый WeakEventHandlerManager с библиотеки Prism 5. Рассмотрим простой пример, в котором generic WeakEventManager добавляет слушатель на какое-то событие.
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        WeakEventManager<WindowEventArgs>
            .AddHandler(this"Loaded", MainWindow_Loaded);
    }

    void MainWindow_Loaded(object sender, EventArgs e)
    {
        MessageBox.Show("Hello World");
    }
}
Теперь более детально остановимся на том, что здесь происходит. WeakEventManager выступает в данном случае в виде диспетчера, в роле прослушивателя (добавление, удаление событий) выступает класс WeakEventListener. Диспетчер события подписывается на само событие и передает вызовы прослушивателям, то есть, является своего рода прослойкой между источником и получателем, разрывая жесткую связь. Дженерик реализация класса WeakEventManager приведена ниже.
public class WeakEventManager<TEventSource, TEventArgs> : WeakEventManager
    where TEventArgs : EventArgs
{
    #region Constructors

    //
    //  Constructors
    //

    private WeakEventManager(string eventName)
    {
        _eventName = eventName;
        _eventInfo = typeof(TEventSource).GetEvent(_eventName);

        if (_eventInfo == null)
            throw new ArgumentException(SR.Get(SRID.EventNotFound, typeof(TEventSource).FullName, eventName));

        _handler = Delegate.CreateDelegate(_eventInfo.EventHandlerType, this, DeliverEventMethodInfo);
    }

    #endregion Constructors

    #region Public Methods

    //
    //  Public Methods
    //

    /// <summary>
    /// Add a handler for the given source's event.
    /// </summary>
    public static void AddHandler(TEventSource source, string eventName, EventHandler<TEventArgs> handler)
    {
        if (handler == null)
            throw new ArgumentNullException("handler");

        CurrentManager(eventName).ProtectedAddHandler(source, handler);
    }

    /// <summary>
    /// Remove a handler for the given source's event.
    /// </summary>
    public static void RemoveHandler(TEventSource source, string eventName, EventHandler<TEventArgs> handler)
    {
        if (handler == null)
            throw new ArgumentNullException("handler");

        CurrentManager(eventName).ProtectedRemoveHandler(source, handler);
    }

    #endregion Public Methods

    #region Protected Methods

    //
    //  Protected Methods
    //

    /// <summary>
    /// Return a new list to hold listeners to the event.
    /// </summary>
    protected override ListenerList NewListenerList()
    {
        return new ListenerList<TEventArgs>();
    }

    /// <summary>
    /// Listen to the given source for the event.
    /// </summary>
    protected override void StartListening(object source)
    {
        _eventInfo.AddEventHandler(source, _handler);
    }

    /// <summary>
    /// Stop listening to the given source for the event.
    /// </summary>
    protected override void StopListening(object source)
    {
        _eventInfo.RemoveEventHandler(source, _handler);
    }

    #endregion Protected Methods

    #region Private Properties

    //
    //  Private Properties
    //

    // get the event manager for the current thread
    private static WeakEventManager<TEventSource, TEventArgs> CurrentManager(string eventName)
    {
        Type managerType = typeof(WeakEventManager<TEventSource, TEventArgs>);
        WeakEventManager<TEventSource, TEventArgs> manager = (WeakEventManager<TEventSource, TEventArgs>)GetCurrentManager(typeof(TEventSource), eventName);

        // at first use, create and register a new manager
        if (manager == null)
        {
            manager = new WeakEventManager<TEventSource, TEventArgs>(eventName);
            SetCurrentManager(typeof(TEventSource), eventName, manager);
        }

        return manager;
    }

    #endregion Private Properties

    #region Private Data

    Delegate _handler;
    string _eventName;
    EventInfo _eventInfo;

    #endregion Private Data
}
Если мы внимательно посмотрим на приведённый выше код, то можем выделить несколько важных частей. Первая – это методы AddHandler и RemoveHandler. Они позволяют добавить событие на прослушку с помощью метода AddHandler и удалять с помощью метода RemoveHandler. В этом же классе переопределены методы StartListening для для добавления события в файл EventInfo, и метод StopListening для удаления с класса EventInfo. Метод AddHandler вызывает в своей реализации функцию StartListening, а метод RemoveHandler соответственно функцию StopListening. Давайте внимательно посмотрим на реализацию метода AddHandler.
public static void AddHandler(TEventSource source, string eventName, EventHandler<TEventArgs> handler)
{
    if (handler == null)
        throw new ArgumentNullException("handler");

    CurrentManager(eventName).ProtectedAddHandler(source, handler);
}
Первым делом мы смотрим, зарегистрирован ли у нас какой-то менеджер, вызывая метод CurrentManager; если такого менеджера нет, то он будет просто создан, как показано на рисунке ниже.
private static WeakEventManager<TEventSource, TEventArgs> CurrentManager(string eventName)
{
    Type managerType = typeof(WeakEventManager<TEventSource, TEventArgs>);
    WeakEventManager<TEventSource, TEventArgs> manager = (WeakEventManager<TEventSource, TEventArgs>)GetCurrentManager(typeof(TEventSource), eventName);

    // at first use, create and register a new manager
    if (manager == null)
    {
        manager = new WeakEventManager<TEventSource, TEventArgs>(eventName);
        SetCurrentManager(typeof(TEventSource), eventName, manager);
    }

    return manager;
}
В этом кроется небольшая проблема. Для каждого события мы, по сути, создаем свой диспетчер событий. А эти менеджеры очень громоздкие в создании. Хотя можно создать один менеджер и использовать его для нескольких событий.
Следующим этапом после получения текущего менеджера для работы с событиями мы вызываем метод ProtectedAddHandler.
private void AddListener(object source, IWeakEventListener listener, Delegate handler)
{
    object sourceKey = (source != null) ? source : StaticSource;

    using (Table.WriteLock)
    {
        ListenerList list = (ListenerList)Table[this, sourceKey];

        if (list == null)
        {
            // no entry in the table - add a new one
            list = NewListenerList();
            Table[this, sourceKey] = list;

            // listen for the desired event
            StartListening(source);
        }

        // make sure list is ready for writing
        if (ListenerList.PrepareForWriting(ref list))
        {
            Table[this, source] = list;
        }

        // add a target to the list of listeners
        if (handler != null)
        {
            list.AddHandler(handler);
        }
        else
        {
            list.Add(listener);
        }

        // schedule a cleanup pass (heuristic (b) described above)
        ScheduleCleanup();
    }
}
Здесь мы видим использование класса ListenerList, в который добавляем нужное нам событие. Если у нас не создан класс ListenerList, мы создаем его, вызывая метод NewListenerList. На этом классе нужно остановиться более детально. 
Каждый объект, который желает подписаться на событие, должен реализовать интерфейс IWeakEventListener. Этот интерфейс содержит один единственный метод ReceiveWeakEvent. Нужно проверить тип диспетчера событий, вызвать обработчик и вернуть true. Если вы не можете определить тип диспетчера, то должны вернуть false. В этом случае будет вызвано исключение System.ExecutionEngineException с несколько непонятным текстом о причине ошибки. По нему становится ясно, что в диспетчерах или прослушивателях есть ошибка. 
Настало время рассказать о том, что же такое ListenerList. ListenerList, если обобщить, являет собой список интерфейсов IWeakEventListener. Так как мы разобрали интересные моменты в методе NewListenerList, давайте продолжим разбор первоначального метода AddListener. Для потокобезопасного добавления слушателя используется паттерн, который называется Readers–writer lock. В языке C# это представлено классами ReaderWriterLock и ReaderWriterLockSlim. Решение, которое используется в данном коде, называется Disposable Pattern и способ освобождения ресурсов через создание врапера, который возвращает IDisposable объект, и конструктор которого захватывает блокировку. Очень занимательно об этом написано в статье Сергея Теплякова "RAII в C#. Локальный Метод Шаблона vs. IDisposable". Пожалуй, процитирую Теплякова по поводу использования такого подхода.
С одной стороны, такой подход полностью оправдан, поскольку он удобен и безопасен с точки зрения исключений. С другой стороны, тот же Эрик Липперт в аннотированной спецификации языка C# ("The C# Programming Language" by Anders Hejlsberg) предостерегает от подобного использования Disposable-объектов.
В самом .NET Framework реализована обертка над ReaderWriterLock, которая приведена в коде ниже.
internal class ReaderWriterLockWrapper
{
    //------------------------------------------------------
    //
    //  Constructors
    //
    //------------------------------------------------------

    #region Constructors

    internal ReaderWriterLockWrapper()
    {
        _rwLock = new ReaderWriterLock();
        _awr = new AutoWriterRelease(_rwLock);
        _arr = new AutoReaderRelease(_rwLock);
    }

    #endregion Constructors

    //------------------------------------------------------
    //
    //  Internal Properties
    //
    //------------------------------------------------------

    #region Internal Properties

    internal IDisposable WriteLock
    {
        get
        {
            _rwLock.AcquireWriterLock(Timeout.Infinite);
            return _awr;
        }
    }

    internal IDisposable ReadLock
    {
        get
        {
            _rwLock.AcquireReaderLock(Timeout.Infinite);
            return _arr;
        }
    }

    #endregion Internal Properties

    //------------------------------------------------------
    //
    //  Private Fields
    //
    //------------------------------------------------------

    #region Private Fields

    private ReaderWriterLock _rwLock;
    private AutoReaderRelease _arr;
    private AutoWriterRelease _awr;

    #endregion Private Fields

    //------------------------------------------------------
    //
    //  Private Classes & Structs
    //
    //------------------------------------------------------

    #region Private Classes & Structs

    private struct AutoWriterRelease : IDisposable
    {
        public AutoWriterRelease(ReaderWriterLock rwLock)
        {
            _lock = rwLock;
        }

        public void Dispose()
        {
            _lock.ReleaseWriterLock();
        }

        private ReaderWriterLock _lock;
    }

    private struct AutoReaderRelease : IDisposable
    {
        public AutoReaderRelease(ReaderWriterLock rwLock)
        {
            _lock = rwLock;
        }

        public void Dispose()
        {
            _lock.ReleaseReaderLock();
        }

        private ReaderWriterLock _lock;
    }
    #endregion Private Classes
}
В конструкторе нашего враппера мы используем такие врапперы, как AutoReaderRelease и AutoWriterRealease. Они используют подход с реализацией метода IDisposable. Эти классы реализованы в регионе "Private Classes & Struct". Прошу прощения, что немного отвлёкся от темы. Очень хотелось поделиться таким занимательным фактом.
Давайте разберем все-таки до конца наш метод AddListener. Как я расписал выше, у нас реализован паттерн, который позволяет иметь множество читателей и один или несколько писателей. Это такой себе продвинутый способ лока объектов. Здесь большую роль играет объект Table класса WeakEventTable, который использует индексированные свойства, для того чтобы управлять соответствием между типом события и менеджером событий в поддержке с "Weak Event Listener" паттерном.  Ниже приведена краткая реализация этих индексаторов.
/// <summary>
/// Get or set the manager instance for the given event.
/// </summary>
internal WeakEventManager this[Type eventSourceType, string eventName]
{
    get
    {
        EventNameKey key = new EventNameKey(eventSourceType, eventName);
        return (WeakEventManager)_eventNameTable[key];
    }

    set
    {
        EventNameKey key = new EventNameKey(eventSourceType, eventName);
        _eventNameTable[key] = value;
    }
}

/// <summary>
/// Get or set the data stored by the given manager for the given source.
/// </summary>
internal object this[WeakEventManager manager, object source]
{
    get
    {
        EventKey key = new EventKey(manager, source);
        object result = _dataTable[key];
        return result;
    }

    set
    {
        EventKey key = new EventKey(manager, source, true);
        _dataTable[key] = value;
    }
}
Если вкратце, то те события, которые нам приходят в наш класс WeakEventTable, просто складываем в Hashtable. По сути, это что-то вроде кастомного ServiceLocator. Выглядит, по сути, это отнюдь не интуитивно понятно, но реализовано в целом неплохо, с учетов всех сценариев, кучи подстраховок и т.д., Пожалуй, из минусов можно отнести излишнюю тяжеловесность реализации,ь хотя большинство кастомных реализаций, которые доводилось видеть мне, работают по похожему сценарию, только список для хранения связей между типом события и самим событием обычно глобальный. ServiceLocator практически в чистом своем виде. Пожалуй, не буду рассматривать метод RemoveListener, так как он аналогичен методу AddListener, только с той разницей, что Add добавляет новый тип события, а метод с приставкой Remove удаляет. По коду ниже все также предельно просто.
private void RemoveListener(object source, object target, Delegate handler)
{
    object sourceKey = (source != null) ? source : StaticSource;

    using (Table.WriteLock)
    {
        ListenerList list = (ListenerList)Table[this, sourceKey];

        if (list != null)
        {
            // make sure list is ready for writing
            if (ListenerList.PrepareForWriting(ref list))
            {
                Table[this, sourceKey] = list;
            }

            // remove the target from the list of listeners
            if (handler != null)
            {
                list.RemoveHandler(handler);
            }
            else
            {
                list.Remove((IWeakEventListener)target);
            }

            // after removing the last listener, stop listening
            if (list.IsEmpty)
            {
                Table.Remove(this, sourceKey);

                StopListening(source);
            }
        }
    }
}
Так как мы рассмотрели работу с классом WeakEventManager, предлагаю перейти к рассмотрению класса WeakEventHandlerManager, чтобы увидеть, как в этом плане поработали ребята с Patterns & Practices в библиотеке Prism. Ниже приведена реализация данного класса.
public static class WeakEventHandlerManager
{
    private static readonly SynchronizationContext syncContext = SynchronizationContext.Current;

    ///<summary>
    /// Invokes the handlers 
    ///</summary>
    ///<param name="sender"></param>
    ///<param name="handlers"></param>
    public static void CallWeakReferenceHandlers(object sender, List<WeakReference> handlers)
    {
        if (handlers != null)
        {
            // Take a snapshot of the handlers before we call out to them since the handlers
            // could cause the array to me modified while we are reading it.
            EventHandler[] callees = new EventHandler[handlers.Count];
            int count = 0;

            //Clean up handlers
            count = CleanupOldHandlers(handlers, callees, count);

            // Call the handlers that we snapshotted
            for (int i = 0; i < count; i++)
            {
                CallHandler(sender, callees[i]);
            }
        }
    }

    private static void CallHandler(object sender, EventHandler eventHandler)
    {
        if (eventHandler != null)
        {
            if (syncContext != null)
            {
                syncContext.Post((o) => eventHandler(sender, EventArgs.Empty), null);
            }
            else
            {
                eventHandler(sender, EventArgs.Empty);
            }
        }
    }

    private static int CleanupOldHandlers(List<WeakReference> handlers, EventHandler[] callees, int count)
    {
        for (int i = handlers.Count - 1; i >= 0; i--)
        {
            WeakReference reference = handlers[i];
            EventHandler handler = reference.Target as EventHandler;
            if (handler == null)
            {
                // Clean up old handlers that have been collected
                handlers.RemoveAt(i);
            }
            else
            {
                callees[count] = handler;
                count++;
            }
        }
        return count;
    }

    ///<summary>
    /// Adds a handler to the supplied list in a weak way.
    ///</summary>
    ///<param name="handlers">Existing handler list.  It will be created if null.</param>
    ///<param name="handler">Handler to add.</param>
    ///<param name="defaultListSize">Default list size.</param>
    public static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
    {
        if (handlers == null)
        {
            handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
        }

        handlers.Add(new WeakReference(handler));
    }

    ///<summary>
    /// Removes an event handler from the reference list.
    ///</summary>
    ///<param name="handlers">Handler list to remove reference from.</param>
    ///<param name="handler">Handler to remove.</param>
    public static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
    {
        if (handlers != null)
        {
            for (int i = handlers.Count - 1; i >= 0; i--)
            {
                WeakReference reference = handlers[i];
                EventHandler existingHandler = reference.Target as EventHandler;
                if ((existingHandler == null) || (existingHandler == handler))
                {
                    // Clean up old handlers that have been collected
                    // in addition to the handler that is to be removed.
                    handlers.RemoveAt(i);
                }
            }
        }
    }
}
Реализация предельно простая, и заметно, что ребята с Patterns & Practices особо не заморачивались в плане реализации. Постараюсь в самом конце рассказать о недостаках данного класса, а пока рассмотрим функции, которые нам доступны. Основные функции, которые зачастую будете использовать вы, если будете использовать данный класс,  это функции AddWeakReferenceHandler для добавления ссылки на собитие и RemoveWeakReferenceHandler для удаления ссылки на событие. Для того чтобы вызвать наши события, нужно вызвать метод CallWeakReferehceHandlers. А уже метод CallWeakReferehceHandlers сначала подчищает те события, на которых ничто не ссылается, а лишь после того вызывает private method CallHandler в цикле для каждого события. 
В целом работа с данными функциями выглядит следующим образом. С помощью метода AddWeakReferenceHandler регистрируем наши события, после чего получим список List<WeakReference>, содержащий мягкие ссылки на наши события. Полученный список List<WeakReference> мы передаем в метод CallWeakReferehceHandlers. Когда нужно удалить ссылки на наши события, вызываем метод RemoveWeakReferenceHandler. Как по мне, эта реализация реально недоработанная. Объект у нас будет жить до тех пор, пока на него не пропадет ссылка. Хотя это лишь недостаток, но не основная проблема. Как мы помним, события, особенно UI котролов, в WPF работают в основном потоке. С этим выплывает вторая проблема, которую описали в документации разработчики Patterns & Practices. Она заключается в использовании класса SynchronizationContext. Если вы использовали в своих приложениях таски, то вам, вероятно, приходилось сталкиваться с использованием данного класса. Разве что вы использовали для обновления UI класс Dispatcher, тогда есть большая вероятность, что вы никогда не слышали о данном классе. Этот класс используется для синхронизации контекста потока, в котором будут выполняться ваши события.
The WeakEventHandlerManager must be first constructed on the UI thread to properly acquire a reference to the UI thread’s SynchronizationContext.
Предупреждение выше гласит, что для того чтобы использовать правильно класс WeakEventHandlerManager, вам необходимо первый раз использовать этот класс в UI потоке. Но тут кроется другая проблема. Если вы хотите выполнить какие-то события не в основном UI потоке, то у вас это не выйдет, по той причине, что экземпляр класса SynchronizationContext, который используется в классе WeakEventHandlerManager, статический. Если мы инициируем впервые наш класс не в UI потоке, то если у нас есть обращение к котролам, мы получим зависание, так как запрещено обращение к контролам не с UI потока.  Я часто в данном контексте употребляю слово поток, так как класс SynchronizationContext – это высокоуровневая оболочка над потоками. Тот же самый метод Post класса SynchronizationContext, использование которого показано ниже.
private static void CallHandler(object sender, EventHandler eventHandler)
    {
        if (eventHandler != null)
        {
            if (syncContext != null)
            {
                syncContext.Post((o) => eventHandler(sender, EventArgs.Empty), null);
            }
            else
            {
                eventHandler(sender, EventArgs.Empty);
            }
        }
    }
Не что иное, как обычная оболочка над методом ThreadPool.QueueUserWorkItem.
public virtual void Post(SendOrPostCallback d, Object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}
И что мы в итоге имеем? Каждое событие мы запускаем в отдельном свободном потоке. А так как мы с вами помним, что пул для ThreadPool – это ограниченное значение, то можем получить такую ситуацию, когда наше приложение просто упадет.
Итоги
Хотелось бы кратко подвести итоги об разборе шаблона слабых событий и двух вариантах его реализации. Мы рассмотрели два разных подхода в реализации. И тот, и другой имеет свои преимущества и недостатки. Например, класс WeakEventManager реализован довольно тяжело и тащит за время привязки одного события довольно много разных объектов. Хотя он и реализован практически с использованием различных паттернов прослушки и т.д. Но он сложный для понимания (если вы решите вдруг попытаться понять, что он делает), и тянет за собой множество ненужных классов. Класс WeakEventHandlerManager, наоборот, реализован проще некуда. По сути, сделали обертку над WeakReference и SynchronizationContext – и нормально. Если бы его сделать чуточку продуманней, то получилось бы именно то, что нужно.
Важно помнить такую вещь, что если вы реализуете свой менеджер для мягкого связывания ваших событий, постарайтесь не делать ваш пул для хранения событий глобальным. Иначе вы будете использовать паттерн ServiceLocator или какую-то его модификацию, и в итоге наверняка сделаете что-то хуже, чем те классы, которые мы рассмотрели сегодня. Главное подходить с умом к поставленной задаче. Всегда можно сделать лучше, но иногда при неумелом использовании мы можем только ухудшить ситуацию. 

No comments:

Post a Comment