События С# и безопасность потоков

ОБНОВИТЬ

Начиная с С# 6, ответ на этот вопрос:

SomeEvent?.Invoke(this, e);

Я часто слышу/читаю следующий совет:

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

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Обновлено: я подумал, что, прочитав об оптимизации, это может также потребовать, чтобы член события был неустойчивым, но Джон Скит заявляет в своем ответе, что CLR не оптимизирует копию.

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

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

Фактической последовательностью может быть эта смесь:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Дело в том, что OnTheEvent работает после того, как автор OnTheEvent от подписки, и все же они просто OnTheEvent от подписки, чтобы избежать этого. Конечно, на самом деле нужна специальная реализация событий с соответствующей синхронизацией в add and remove accessors. Кроме того, существует проблема возможных взаимоблокировок, если блокировка сохраняется во время запуска события.

Так что это Cargo Cult Programming? Похоже, что так много людей должны предпринять этот шаг, чтобы защитить свой код от нескольких потоков, когда на самом деле мне кажется, что события требуют гораздо большей осторожности, чем это, прежде чем они могут использоваться как часть многопоточного дизайна, Следовательно, люди, которые не берут на себя такую дополнительную заботу, также могут игнорировать этот совет - это просто не проблема для однопоточных программ, и на самом деле, учитывая отсутствие volatile в большинстве онлайн-примеров кода, совет может иметь никакого эффекта вообще.

(И не проще ли просто назначить пустой delegate { } в объявлении участника, чтобы вам никогда не нужно было проверять null в первую очередь?)

Обновлено: В случае, если это было неясно, я понял намерение совета - избегать исключения нулевой ссылки при любых обстоятельствах. Моя точка зрения заключается в том, что это конкретное исключение ссылочной ссылки может возникнуть только в том случае, если другой поток исключает из события, и единственная причина для этого заключается в том, чтобы гарантировать, что никакие дальнейшие вызовы не будут получены через это событие, что явно НЕ достигается этой методикой, Вы будете скрывать состояние гонки - было бы лучше раскрыть это! Это исключительное исключение помогает обнаружить злоупотребление вашим компонентом. Если вы хотите, чтобы ваш компонент был защищен от злоупотреблений, вы можете следовать примеру WPF - сохранить идентификатор потока в своем конструкторе и затем выбросить исключение, если другой поток попытается напрямую взаимодействовать с вашим компонентом. Или же реализовать действительно поточно-безопасный компонент (не простая задача).

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

Обновление в ответ на сообщения Eric Lippert в блоге:

Итак, главное, что я пропустил о обработчиках событий: "Обработчики событий должны быть надежными перед лицом вызова даже после того, как событие было отменено", и, очевидно, поэтому нам нужно только заботиться о возможности события делегат равен null. Это требование для обработчиков событий документировано где угодно?

Итак: "Существуют другие способы решения этой проблемы, например, инициализация обработчика, чтобы иметь пустое действие, которое никогда не удаляется, но выполнение нулевой проверки - это стандартный шаблон".

Итак, остается один оставшийся фрагмент моего вопроса, почему явный-нуль-проверить "стандартный шаблон"? Альтернатива, назначающая пустой делегат, требует, чтобы в объявлении события добавлялось только = delegate {}, и это устраняет эти маленькие кучки вонючей церемонии из каждого места, где происходит событие. Было бы легко убедиться, что пустой делегат дешев для создания экземпляра. Или я все еще что-то пропущу?

Несомненно, это должно быть (как предложил Джон Скит), это просто совет.NET 1.x, который не вымер, как это должно было быть сделано в 2005 году?

Ответ 1

JIT не разрешает выполнять оптимизацию, о которой вы говорите в первой части, из-за условия. Я знаю, что это было создано как призрак некоторое время назад, но это недействительно. (Я проверил его с Джо Даффи или Вэнсом Моррисоном некоторое время назад, я не могу вспомнить, кто.)

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

И да, конечно, состояние гонки - но всегда будет. Предположим, мы просто изменили код на:

TheEvent(this, EventArgs.Empty);

Теперь предположим, что список вызовов для этого делегата имеет 1000 записей. Вполне возможно, что действие в начале списка будет выполнено до того, как другой поток отменит подписчику в конце списка. Однако этот обработчик все равно будет выполнен, потому что это будет новый список. (Делегаты неизменны.) Насколько я вижу, это неизбежно.

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

Ответ 2

Я вижу, что многие люди идут в сторону метода расширения этого...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Это дает вам лучший синтаксис для создания события...

MyEvent.Raise( this, new MyEventArgs() );

И также удаляет локальную копию, так как она захватывается при вызове метода.

Ответ 3

"Почему явная-нуль-проверка" стандартного шаблона "?"

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

Если вы всегда подписываете пустой делегат на свои события, когда они создаются, будут некоторые накладные расходы:

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

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

Я провел небольшое тестирование производительности, чтобы увидеть влияние подхода subscribe-empty-delegate, и вот мои результаты:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

Обратите внимание, что для случая с нулевым или одним абонентом (обычным для элементов управления пользовательского интерфейса, где события многочисленны), событие, предварительно инициализированное пустым делегатом, заметно медленнее (более 50 миллионов итераций...)

Для получения дополнительной информации и исходного кода посетите этот пост в блоге . NET-событий безопасности потоков, которые я опубликовал только за день до того, как этот вопрос был спросил (!)

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

Ответ 4

Мне действительно понравилось это читать - нет! Хотя мне нужно, чтобы он работал с функцией С#, называемой событиями!

Почему бы не исправить это в компиляторе? Я знаю, что есть люди MS, которые читают эти сообщения, поэтому, пожалуйста, не пламените это!

1 - проблема с Null). Почему бы не сделать события не такими .Empty, а не null в первую очередь? Сколько строк кода будет сохранено для нулевой проверки или должно вставить = delegate {} в объявление? Пусть компилятор обрабатывает пустой случай, IE ничего не делает! Если все это имеет значение для создателя события, они могут проверить .Empty и делать все, что им нужно! В противном случае все добавления null check/delegate будут взломать проблему!

Честно говоря, я устал от того, чтобы делать это с каждым событием - как шаблонный код!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - проблема с расы). Я читаю сообщение в блоге Eric, я согласен с тем, что обработчик H (обработчик) должен обрабатывать, когда он сам разыгрывает, но не может ли это событие быть неизменным/потокобезопасным? IE, установите флаг блокировки при его создании, так что всякий раз, когда он вызывается, он блокирует все подписки и не подписался на него во время его выполнения?

Заключение

Разве современные языки не должны решать такие проблемы для нас?

Ответ 5

Согласно Джеффри Рихтеру в книге CLR через С#, правильный метод:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

Потому что он создает ссылочную копию. Для получения дополнительной информации см. Раздел "Событие" в книге.

Ответ 6

Я использовал этот шаблон дизайна, чтобы гарантировать, что обработчики событий не будут выполнены после того, как они будут отписаны. До сих пор он работал очень хорошо, хотя я не пробовал профилирование производительности.

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

В основном я работаю с Mono для Android, и Android, похоже, не нравится, когда вы пытаетесь обновить представление после того, как его активность была отправлена ​​на задний план.

Ответ 7

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

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

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

Ответ 8

Так что я немного опаздываю на вечеринку здесь.:)

Что касается использования нулевого, а не нулевого шаблона объекта для представления событий без подписчиков, рассмотрите этот сценарий. Вам нужно вызвать событие, но построение объекта (EventArgs) является нетривиальным, и в общем случае ваше событие не имеет подписчиков. Было бы полезно, если бы вы могли оптимизировать свой код, чтобы проверить, были ли у вас какие-либо подписчики, прежде чем вы приложили усилия по обработке аргументов и вызвали событие.

Имея это в виду, решение состоит в том, чтобы сказать "ну, нулевые подписчики представлены нулем". Затем просто выполните нулевую проверку перед выполнением дорогостоящей операции. Я полагаю, что другой способ сделать это состоял бы в том, чтобы иметь свойство Count в типе делегирования, поэтому вы выполнили бы дорогостоящую операцию, если myDelegate.Count > 0. Использование свойства Count - несколько приятный шаблон, который решает исходную проблему разрешающей оптимизации, а также обладает хорошим свойством быть вызванным без возникновения исключения NullReferenceException.

Имейте в виду, что, поскольку делегаты являются ссылочными типами, им разрешено иметь значение null. Возможно, нет простого способа скрыть этот факт под обложками и поддерживать только шаблон нулевого объекта для событий, поэтому альтернатива, возможно, заставила разработчиков проверить как нулевые, так и нулевые подписчики. Это было бы даже уродливее, чем текущая ситуация.

Примечание. Это чистая спекуляция. Я не участвую в языках .NET или CLR.

Ответ 10

для однопоточных приложений, вы соответствуете, это не проблема.

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

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

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

Если вы не используете временную переменную и не используете пустой делегат, а кто-то отменил подписку, вы получите исключение с нулевой ссылкой, которое является фатальным, поэтому я думаю, что стоимость того стоит.

Ответ 11

Я никогда не считал, что это большая проблема, потому что я вообще только защищаю от такого рода потенциальной неудачи потоков в статических методах (и т.д.) для моих повторно используемых компонентов, и я не делаю статические события.

Я делаю это неправильно?

Ответ 12

Проведите все ваши мероприятия при строительстве и оставьте их в покое. Конструкция класса Delegate не может правильно обрабатывать любое другое использование, как я объясню в последнем абзаце этого сообщения.

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

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

Единственный раз, когда обработчик не должен быть уведомлен о том, что произошло событие, - это если событие на самом деле не произошло! Поэтому, если вы не хотите, чтобы обработчик был уведомлен, прекратите генерировать события (т.е. Отключите элемент управления или все, что отвечает за обнаружение и появление события в первую очередь).

Честно говоря, я считаю, что класс делегата недостоверный. Слияние/переход к MulticastDelegate было огромной ошибкой, потому что оно эффективно меняло (полезное) определение события из того, что происходит в один момент времени, к чему-то, что происходит в течение промежутка времени. Для такого изменения требуется механизм синхронизации, который может логически свернуть его обратно в один момент, но в MulticastDelegate отсутствует такой механизм. Синхронизация должна охватывать весь промежуток времени или мгновение, когда происходит событие, так что, как только приложение принимает синхронизированное решение для начала обработки события, оно полностью обрабатывает его (транзакционно). С черным ящиком, который является гибридным классом MulticastDelegate/Delegate, это почти невозможно, поэтому придерживается использования одного абонента и/или реализует свой собственный вид MulticastDelegate, который имеет дескриптор синхронизации, который можно удалить, пока используется/модифицирована цепочка обработчиков. Я рекомендую это, потому что альтернативой было бы избыточно реализовать синхронизацию/транзакционную целостность во всех ваших обработчиках, что было бы смешно/излишне сложным.

Ответ 13

Пожалуйста, смотрите здесь: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Это правильное решение и всегда должно использоваться вместо всех других обходных решений.

"Вы можете убедиться, что в списке внутренних вызовов всегда есть хотя бы один элемент, инициализируя его анонимным методом do-nothing. Поскольку никакая внешняя сторона не может ссылаться на анонимный метод, никакая внешняя сторона не может удалить этот метод, поэтому делегат никогда не будет нулевым"   - Программирование компонентов .NET, второе издание, Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

Ответ 14

Я не верю, что вопрос ограничен типом С# "event". Устранив это ограничение, почему бы не изобретать колесо немного и не сделать что-то в этом направлении?

Безопасное повышение потока событий - наилучшая практика

  • Возможность подчинить/отменить подписку на любой поток во время рейза (раса состояние удалено)
  • Оператор перегружает для + = и - = на уровне класса.
  • Общий делегат, определенный делегатом

Ответ 15

Спасибо за полезное обсуждение. Недавно я работал над этой проблемой и сделал следующий класс, который немного медленнее, но позволяет избежать вызова к расположенным объектам.

Основной смысл здесь состоит в том, что список вызовов может быть изменен, даже если событие поднято.

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

И используется следующее:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Test

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

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

В конструкторе Bar (объект-слушатель) я подписываюсь на SomeEvent (который реализован, как показано выше) и отписывается в Dispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

Также у меня есть пара потоков, которые вызывают событие в цикле.

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

Если были условия гонки, я должен увидеть сообщение в консоли, но оно пустое. Но если я использую события clr, как обычно, я вижу, что он заполнен предупреждающими сообщениями. Итак, я могу заключить, что в С# можно реализовать потокобезопасные события.

Как вы думаете?