Действительно ли это - допустимый шаблон для того, чтобы сгенерировать события в C#?

Обновление: В пользу любого читающего это, начиная с.NET 4, блокировка является ненужным из-за изменений в синхронизации автоматически сгенерированных событий, таким образом, я просто использую это теперь:

public static void Raise(this EventHandler handler, object sender, T e) where T : EventArgs
{
    if (handler != null)
    {
        handler(sender, e);
    }
}

И повысить его:

SomeEvent.Raise(this, new FooEventArgs());

Будучи читающей из статей Jon Skeet о многопоточности, я попытался инкапсулировать подход, как который он рекомендует генерированию события в дополнительном методе так (с подобной универсальной версией):

public static void Raise(this EventHandler handler, object @lock, object sender, EventArgs e)
{
    EventHandler handlerCopy;
    lock (@lock)
    {
        handlerCopy = handler;
    }

    if (handlerCopy != null)
    {
        handlerCopy(sender, e);
    }
}

Как это можно затем назвать так:

protected virtual void OnSomeEvent(EventArgs e)
{
    this.someEvent.Raise(this.eventLock, this, e);
}

Есть ли какие-либо проблемы с выполнением этого?

Кроме того, я немного смущен необходимостью блокировки во-первых. Насколько я понимаю делегат копируется в примере в статье для предотвращения возможности его изменение (и становление пустым) между пустой проверкой и вызовом делегата. Однако у меня создалось впечатление, что доступ/присвоение этого вида является атомарным, итак, почему блокировка необходима?

Обновление: Относительно комментария Mark Simpson ниже, я бросил вместе тест:

static class Program
{
    private static Action foo;
    private static Action bar;
    private static Action test;

    static void Main(string[] args)
    {
        foo = () => Console.WriteLine("Foo");
        bar = () => Console.WriteLine("Bar");

        test += foo;
        test += bar;

        test.Test();

        Console.ReadKey(true);
    }

    public static void Test(this Action action)
    {
        action();

        test -= foo;
        Console.WriteLine();

        action();
    }
}

Это производит:

Foo
Bar

Foo
Bar

Это иллюстрирует что параметр делегата к методу (action) не зеркально отражает аргумент, который был передан в него (test), который отчасти ожидается, я предполагаю. Моим вопросом является желание это влияние законность блокировки в контексте моего Raise дополнительный метод?

Обновление: Вот код, который я теперь использую. Это не совсем столь же изящно, как мне понравилось бы, но это, кажется, работает:

public static void Raise(this object sender, ref EventHandler handler, object eventLock, T e) where T : EventArgs
{
    EventHandler copy;
    lock (eventLock)
    {
        copy = handler;
    }

    if (copy != null)
    {
        copy(sender, e);
    }
}

10
задан Will Vousden 8 September 2015 в 19:03
поделиться

6 ответов

Цель блокировки состоит в том, чтобы поддерживать безопасность потоков, когда вы переопределяете проволоки события по умолчанию. Извинения, если некоторые из этого объясняют вещи, которые вы уже могли сделать вывод из статьи Джона; Я просто хочу убедиться, что я полностью ясно обо всем.

Если вы объявляете своими событиями, такими, как это:

public event EventHandler Click;

, то подписки на мероприятие автоматически синхронизируются с блокировкой (это) . Вы делаете Не нужно написать любой специальный код блокировки, чтобы вызвать обработчик событий. Это совершенно приемлемо, чтобы написать:

var clickHandler = Click;
if (clickHandler != null)
{
    clickHandler(this, e);
}

Однако , если вы решите переопределить события по умолчанию, I.E. :

public event EventHandler Click
{
    add { click += value; }
    remove { click -= value; }
}

wnow У вас есть проблема, потому что больше нет неявного блокировки. Ваш обработчик событий только что потерял свою резьбу безопасности. Вот почему вам нужно использовать замок:

public event EventHandler Click
{
    add
    {
        lock (someLock)      // Normally generated as lock (this)
        {
            _click += value;
        }
    }
    remove
    {
        lock (someLock)
        {
            _click -= value;
        }
    }
}

Лично, я не беспокоюсь об этом, но обоснование Джона - это звук. Тем не менее, у нас есть небольшая проблема . Если вы используете Private EventHandler , чтобы сохранить свое мероприятие, вы, вероятно, у вас, вероятно, у вас есть код в классе, который делает это:

protected virtual void OnClick(EventArgs e)
{
    EventHandler handler = _click;
    if (handler != null)
    {
        handler(this, e);
    }
}

Это плохо , потому что мы получаем доступ к Такое же частное поле хранения без использования того же блокировки, которое использует свойство .

Если какой-то код внешний к классу идет:

MyControl.Click += MyClickHandler;

Этот внешний код, проходящий через общественное свойство, уважает замок. Но Вы не , потому что вы вместо этого касаетесь частного поля.

Назначение . Часть Часть ClickHandler = _Click является атомным, да, но во время этого назначения поля _Click может быть в переходном состоянии, один Это было наполовину написано внешним классом. Когда вы синхронизируете доступ к полю, недостаточно только для синхронизации доступа для записи, вы должны также синхронизировать доступ для чтения:

protected virtual void OnClick(EventArgs e)
{
    EventHandler handler;
    lock (someLock)
    {
        handler = _click;
    }
    if (handler != null)
    {
        handler(this, e);
    }
}

Обновление

Оказывается, что некоторые из диалоговых полет вокруг комментариев были фактически правильными, как доказано обновлением ОП. Это не проблема с методами расширения как SE, это тот факт, что делегаты имеют семантику Value типа и копируются на задание. Даже если вы взяли это из метода расширения и просто вызывали его как статический метод, вы получите то же поведение.

Вы Можно Обойти на это ограничение (или функцию, в зависимости от вашей точки зрения) со статическим методом утилиты, хотя я уверен, что вы не могут Отказ Вот статический метод, который будет работать:

public static void RaiseEvent(ref EventHandler handler, object sync,
    object sender, EventArgs e)
{
    EventHandler handlerCopy;
    lock (sync)
    {
        handlerCopy = handler;
    }
    if (handlerCopy != null)
    {
        handlerCopy(sender, e);
    }
}

Эта версия работает, потому что мы на самом деле не передаем EventHandler , просто ссылка на него ( примечание в метод подписи ). К сожалению, вы не можете использовать с с этим в методе расширения, поэтому ему придется оставаться простой статическим методом.

(И, как указано ранее, вы должны убедиться, что вы передаете тот же объект блокировки в качестве параметра Sync , который вы используете в ваших публичных событиях; если вы проходите любой другой объект, то все обсуждение мут.)

7
ответ дан 4 December 2019 в 01:01
поделиться
- [11284949-

Я понимаю, что я не отвечаю на ваш вопрос, но более простой подход к устранению возможности необходимости исключения от нулевого контроля при повышении события - это установить все события, равные делегации {} на сайте их декларации. Например:

public event Action foo = delegate { };
2
ответ дан 4 December 2019 в 01:01
поделиться
lock (@lock)
{
    handlerCopy = handler;
}

Назначение основных типов, таких как ссылки, являются атомными, поэтому здесь нет точки использования блокировки здесь.

1
ответ дан 4 December 2019 в 01:01
поделиться

Я не убежден в действительности приема копии, чтобы избежать нулей. Событие идет NULL, когда все подписчики сообщите своему классу не говорить с ними. NULL означает Никто не хочет услышать ваше мероприятие. Может быть, прослушивание объекта только что было расположено. В этом случае копирование обработчика просто продвигает проблему о. Теперь, вместо того, чтобы позвонить на нулю, вы получите звонок на обработчик событий, который пытался отписаться от события. Вызов скопированного обработчика просто перемещает проблему из издателя в абонент.

Мое предложение - просто поставить на него постаратьсяться;

    if (handler != null)
    {
        try
        {
            handler(this, e);
        }
        catch(NullReferenceException)
        {
        }
    }

Я также думал, что проверим, как Microsoft поднимает самое важное событие; нажав кнопку. Они просто делают это, в базе Control.OnClick ;

protected virtual void OnClick(EventArgs e)
{
    EventHandler handler = (EventHandler) base.Events[EventClick];
    if (handler != null)
    {
        handler(this, e);
    }
}

Итак, они скопируют обработчик, но не блокируют его.

-2
ответ дан 4 December 2019 в 01:01
поделиться

События «Безопасные резьбы» могут стать довольно сложными. Есть пара разных вопросов, которые вы потенциально можете столкнуться с:

NullReferenceException

Последний подписчик может отписаться между вашим нулевым чеком и вызовом делегата, вызывая NullReferenceException. Это довольно простое решение, вы можете либо блокироваться вокруг вызовов (не отличная идея, поскольку вы вызываете внешний код)

// DO NOT USE - this can cause deadlocks
void OnEvent(EventArgs e) {
    // lock on this, since that's what the compiler uses. 
    // Otherwise, use custom add/remove handlers and lock on something else.
    // You still have the possibility of deadlocks, though, because your subscriber
    // may add/remove handlers in their event handler.
    //
    // lock(this) makes sure that our check and call are atomic, and synchronized with the
    // add/remove handlers. No possibility of Event changing between our check and call.
    // 
    lock(this) { 
       if (Event != null) Event(this, e);
    }
}

Скопируйте обработчик (рекомендуемый)

void OnEvent(EventArgs e) {
    // Copy the handler to a private local variable. This prevents our copy from
    // being changed. A subscriber may be added/removed between our copy and call, though.
    var h = Event;
    if (h != null) h(this, e);
}

или имеют нулевой делегат, который всегда подписан.

EventHandler Event = (s, e) => { }; // This syntax may be off...no compiler handy

Обратите внимание, что вариант 2 (копия обработчик) не требует блокировки - поскольку копия атомная, нет шансов на несоответствие там.

Чтобы вернуть это к вашему методу расширения, вы выполняете небольшое изменение опции 2. Ваша копия происходит на вызове метода расширения, поэтому вы можете уйти сразу:

void Raise(this EventHandler handler, object sender, EventArgs e) {
    if (handler != null) handler(sender, e);
}

, возможно, Проблема джиттера, встроенного и удаления временной переменной. Мое ограниченное понимание состоит в том, что это допустимое поведение для <.NET 2.0 или стандартов ECMA - но .NET 2.0+ укрепила гарантии, чтобы сделать его неэффективным - YMMV на моно.

Старинные данные

Хорошо, поэтому мы решили NRE, принимая копию обработчика. Теперь у нас есть второй выпуск несвежей данных. Если абонент отписывается между нами, принимая копию и вызов делегата, то мы все равно позвоним их. Возможно, это неверно. Вариант 1 (блокировка вызовов) решает эту проблему, но риск тупика. Мы вроде застревают - у нас есть 2 разных проблемы, требующие 2 различных решений для того же куска кода.

Поскольку тупики действительно трудно диагностировать и предотвращать, рекомендуется перейти с вариантом 2. Что требует, чтобы CALLEE должен обрабатывать вызываемые даже после отписания. Это должно быть достаточно легко для обработчика, чтобы проверить, что он все еще хочет / умеет вызывать, и выйти чисто, если нет.

Хорошо, так почему же Jon Skeet предлагает замок в oneVent? Он предотвращает чтение кэширования от причины устаревших данных. Вызов для блокировки переводит на мониторинг.enter / Exit, который оба генерируют барьер памяти, который предотвращает перезагрузку чтения / записи и кэшированных данных. Для наших целей они по сути сделают волатильный делегат - это означает, что он не может быть кэширован в регистре процессора и должен быть прочитан из основной памяти для обновленного значения каждый раз. Это предотвращает проблему отписания подписчика, но ценность события кэшируется нитью, который никогда не замечает.

Заключение

Итак, как насчет вашего кода:

void Raise(this EventHandler handler, object @lock, object sender, EventArgs e) {
     EventHandler handlerCopy;
     lock (@lock) {
        handlerCopy = handler;
     }

     if (handlerCopy != null) handlerCopy(sender, e);
}

Ну, вы принимаете копию делегата (дважды на самом деле) и выполняете блокировку, который генерирует барьер памяти. К сожалению, ваш замок принимается при копировании вашей локальной копии - что не сделает ничего для проблем с устаревшей данными Jon Skeet, пытается решить. Вам понадобится что-то вроде:

void Raise(this EventHandler handler, object sender, EventArgs e) {
   if (handler != null) handler(sender, e);
}

void OnEvent(EventArgs e) {
   // turns out, we don't really care what we lock on since
   // we're using it for the implicit memory barrier, 
   // not synchronization     
   EventHandler h;  
   lock(new object()) { 
      h = this.SomeEvent;
   }
   h.Raise(this, e);
}

, который на самом деле не похоже на гораздо меньше кода для меня.

1
ответ дан 4 December 2019 в 01:01
поделиться

Здесь есть несколько проблем, и я буду иметь дело с ними один за раз.

Выпуск № 1: Ваш код, вам нужно заблокировать?

Прежде всего, код, который у вас есть на своем вопросе, нет необходимости в блоке в этом коде.

Другими словами, метод повышения может быть просто переписан к этому:

public static void Raise(this EventHandler handler, object sender, EventArgs e)
{
    if (handler != null)
        handler(sender, e);
}

Причина этого заключается в том, что делегат представляет собой неизменную конструкцию, что означает, что делегат, который вы попадаете в ваш метод, как только вы в этом Метод не изменится на всю жизнь этого метода.

Даже если разная нить состоит в том, чтобы одновременно состоиться с событием, что будет создавать новый делегат. Объект делегата, который у вас в вашем объекте, не изменится.

Так что это проблема № 1, вам нужно заблокировать, если у вас есть код, как вы сделали. Ответ - нет.

Выпуск № 3: Почему вывод от вашего последнего куска кода не изменяется?

Это возвращается к вышеуказанному коду. Метод расширения уже получил свою копию делегата, и эта копия никогда не изменится. Единственный способ «сделать это изменение», не передавая метод копии, но вместо этого, как показано в других ответах здесь, пропустите его псевдонима имя для поля / переменной, содержащей делегат. Только тогда вы соблюдаете изменения.

Вы можете посмотреть на это таким образом:

private int x;

public void Test1()
{
    x = 10;
    Test2(x);
}

public void Test2(int y)
{
    Console.WriteLine("y = " + y);
    x = 5;
    Console.WriteLine("y = " + y);
}

Вы ожидаете, что вы изменитесь на 5 в этом случае? Нет, наверное, нет, и это то же самое с делегатами.

Выпуск № 3: Почему Джон использует блокировку в своем коде?

Так почему же Джон использует блокировку в его : выбирая, что заблокировать ? Ну, его код отличается от вас в том смысле, что он не прошел копию основного делегата где угодно.

В своем коде, который выглядит так:

protected virtual OnSomeEvent(EventArgs e)
{
    SomeEventHandler handler;
    lock (someEventLock)
    {
        handler = someEvent;
    }
    if (handler != null)
    {
        handler (this, e);
    }
}

Есть шанс, что если он не использовал замки, и вместо этого писал код, как это:

protected virtual OnSomeEvent(EventArgs e)
{
    if (handler != null)
        handler (this, e);
}

, то другой нить может изменить «обработчик» между Оценка экспрессии Чтобы выяснить, есть ли какие-либо подписчики, и до фактического вызова, другими словами:

protected virtual OnSomeEvent(EventArgs e)
{
    if (handler != null)
                         <--- a different thread can change "handler" here
        handler (this, e);
}

, если он прошел обработчик до отдельного метода, его код был бы похожим на ваш и, таким образом, требуется без блокировки.

В основном, акт передачи значения делегата в качестве параметра делает копию, этот код «копия» является атомным. Если у вас время разная резьба верна, что разные потоки либо сделают свое изменение во времени, чтобы получить новое значение с вызовом или нет.

Одна из причин использования блокировки, даже если вы звоните, могут быть введены барьер памяти, но я сомневаюсь, что это будет любое влияние на этот код.

Так что это проблема № 3, почему код Джона действительно нужен замок.

Выпуск № 4: как насчет изменения методов доступа к событиям по умолчанию?

Выпуск 4, которые были воспитаны в других ответах, здесь вращаются вокруг необходимости блокировки при переписывании добавления / удаления доступа по умолчанию на событие, Для того, чтобы контролировать логику по любой причине.

В основном, вместо этого:

public event EventHandler EventName;

Вы хотите написать это или некоторое изменение этого:

private EventHandler eventName;
public event EventHandler EventName
{
    add { eventName += value; }
    remove { eventName -= value; }
}

Этот код . нуждается в блокировке, потому что если вы посмотрите на оригинальную реализацию, без переоценки Методы Accessor, вы заметите, что он использует блокировку по умолчанию, тогда как код, который мы написали, не делает.

Мы могли бы оказаться в конечном итоге сценарий выполнения, который выглядит так (помните, что «A + = B» действительно означает «A = A + B»):

Thread 1              Thread 2
read eventName
                      read eventName
add handler1
                      add handler2
write eventName
                      write eventName  <-- oops, handler1 disappeared

Чтобы исправить это, вам нужно блокировка.

0
ответ дан 4 December 2019 в 01:01
поделиться
Другие вопросы по тегам:

Похожие вопросы: