Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
foo.SomeEvent += SomeHandler;
...
foo.SomeEvent -= SomeHandler;
done = true;
//
void SomeHandler() {
if (done) {
throw new InvalidOperationException("Эй, я уже отписан!!!");
}
}
Фактически, любой код, в котором возникает проблема вызова обработчика после отписки от события, чем-то похож на приведенный мною: всегда есть некоторое условие, которое появляется после отписки — и в зависимости от которого происходит ошибка в обработчике.foo.SomeEvent -= SomeHandler; done = true;foo.SomeEvent -= SomeHandler; done = true;public static partial class Extensions {
public static void SafeInvoke(this EventHandler action, EventArgs e) {
if (action != null) action(e);
}
}
...
protected virtual void OnSomeEvent(EventArgs e) {
SomeEvent.SafeInvoke();
}
Уж параметр метода-то ни при каких оптимизациях измениться между проверкой и вызовом не должен…Уж параметр метода-то ни при каких оптимизациях измениться между проверкой и вызовом не должен…А ещё JIT умеет инлайнить методы
А следовательно, защита от вызова события после отписывания от него находится в области ответственности внешнего кода.
Единственным способом, позволяющим полностью обезопасить себя — заставить обработчики событий проверять, не отписались ли они уже от конкретного события.
foo.SomeEvent += SomeHandler;
...
foo.SomeEvent -= SomeHandler;
done = true;
//
void SomeHandler() {
if (done) return;
}
foo.SomeEvent += SomeHandler;
...
foo.SomeEvent -= SomeHandler;
lock(some_lock) {
done = true;
}
//
void SomeHandler() {
lock(some_lock) {
if (done) return;
}
}
Чисто техническая замена внутренней проверки на проверку извне ничего не даст.А потому что не надо менять «чисто технически». Надо думать о том, какие могут возникнуть проблемы — и именно их и решать.
NullReferenceException, практика проверки не является документированной и общепринятой. Поскольку потокобезопасность в данном случае зависит от реализации обработчика — этот метод нельзя назвать полностью потокобезопасным.foo.SomeEvent += SomeHandler;
...
foo.SomeEvent -= SomeHandler;
//
void SomeHandler() {
Console.WriteLine("Hello, world!");
}
using (var res = new SomeResource()) {
var done = false;
var _lock = new object();
Action SomeHandler() = () => {
lock(_lock) if (!done) res.DoSomething();
};
foo.SomeEvent += SomeHandler;
...
foo.SomeEvent -= SomeHandler;
lock(_lock) done = true;
}
Попробуйте переписать, вызвав res.DoSomething(); вне критической секции.А следовательно, защита от вызова события после отписывания от него находится в области ответственности внешнего кода.
void SomeHandler() { Log.Trace("Handled!"); } (то есть не имеющий никаких общих незащищенных ресурсов) — то нам вообще без разницы, вовремя мы его отписываем или нет.Так что было бы хорошо иметь способ полностью себя обезопасить имея контроль лишь над одной из сторон.Так в чем проблема? Проверка локальной переменной на null делает код совершенно безопасным для нас.
null дает ложное чувство безопасности, ощущение что теперь с кодом ничего не случится. Тут важно иметь небольшую тревожную заботу по поводу возможных нестыковок.using (var res = new SharedResource()) {
var handler = new MessageHandler(res);
messaging.Subscribe(foo.SomeEvent, handler);
...
messaging.Unsubscribe(foo.SomeEvent, handler);
}
// где-то внутри MessageHandler
void OnMessage() {
res.DoSomething();
}
Тогда почему не рассматривается возможность получения ArgumentException? Или IOException?
try / catch я тоже рассматривал — в этом случае вызов обработчиков прервется при первом исключении.null не обеспечит полную потокобезопасность вашего кода (как это утверждается в примерах и книгах, в частности в CLR via C#), и это нужно иметь в виду.Implementation is guaranteed to be free of race conditions when accessed by multiple threads simultaneously.
ObjectDisposedException я написал в статье — с точки зрения разработчика вполне логично думать, что у нас никогда не возникнет ситуации, в которой вызовется обработчик освобожденного объекта, и можно просто не городить ненужные по нашему мнению блоки try / catch. Случай, когда обработчик сам ловит свой ObjectDisposedException полностью аналогичен проверке на if(IsDisposed == true).Implementation is guaranteed to be free of race conditions when accessed by multiple threads simultaneously.«Free of race conditions» — слишком расплывчатое определение. Скажем, с моей точки зрения тут никаких гонок внутри объекта нет.
Само использование делегатов вносит состояние гонкиКак?!
Поскольку исход подобного действия зависит от количества работающих с объектом потоков — это является состоянием гонки.Как раз-таки, если скопировать делегат в локальную переменную, то в результате будут вызваны все подписчики, существовавшие на момент появления события (точнее, на момент копирования) независимо от количества потоков.
ObjectDisposedException, поскольку мы не можем доверять обработчикам, которые не знают, что могут быть вызваны после отписки от события. Это приведет к неопределенному поведению нашей системы события в целом. В идеале обработчики либо не должны влиять на процесс вызова события, либо выполнять контракт, согласно которому они обязуются не обрабатывать события, от которых уже отписались. Поскольку ни то, ни другое у нас не обеспечивается — частичная ответственность за безопасность вызова лежит на нас.Почему нас должна волновать причина, по которой вызывается обработчик, который уже отписался от события?Потому что вызов обработчика, отписанного от события, ничем не отличается от отписывания от события обработчика, который уже выполняется (надо просто представить, что у него сейчас выполняется нулевая строчка).
NullReferenceException с «нашей» стороны.event). Тут проблема в самих делегатах, от этого никак не убежать.нет никакой возможности проверить, отписался ли кто-то от события во время вызова или нет
Проблема в том, что для разработчика процесс вызова делегата выглядит атомарным, мы не можем проверять и вызывать подписчики по одному.Забавно, но именно это мы как раз и можем сделать :)
Технически, это может спасти от вызова отписавшихся обработчиков, но мы не можем быть уверенными в том, какие действия сделал объект-подписчик перед тем, как отписаться от события, поэтому мы все еще можем напороться на «испорченный» объект
Да, я тоже в итоге пришел к выводу, что надо ждать завершения обработчика в методе remove. Вот только про static-поле я как-то не понял — а что, если одновременно случится несколько разных событий? Или даже одно и то же событие несколько раз?
Кроме того, тут есть проблема самоотписывания обработчика. Ваше решение в таком случае попросту повиснет (ожидание на событии, которое может установить только тот же самый поток).
Ну и, наконец, мне не нравится идея, что для того, чтобы отписать один обработчик, надо дождаться всех остальных. Все-таки ожидание на отписывании от события — вещь неожиданная, и надо это ожидание свети к минимуму.
public struct EventHelper
{
private event Action handlers;
private event Action<Action> removeHooks;
public event Action Handlers
{
add
{
if (value.GetInvocationList().Length > 1)
throw new ArgumentException("Event handler must be single method");
handlers += value;
}
remove
{
if (value.GetInvocationList().Length > 1)
throw new ArgumentException("Event handler must be single method");
handlers -= value;
var r = removeHooks;
if (r != null) r(value);
}
}
public void Invoke()
{
var h = handlers;
if (h == null) return;
var i = new Invokation(h.GetInvocationList());
removeHooks += i.RemoveHandler;
try
{
i.Invoke();
}
finally
{
removeHooks -= i.RemoveHandler;
}
}
private class Invokation
{
private readonly Delegate[] handlers;
private Delegate currentHandler;
private Delegate waiting;
private Thread ownerThread;
public Invokation(Delegate[] handlers)
{
this.handlers = handlers;
}
public void Invoke()
{
this.ownerThread = Thread.CurrentThread;
for (var i = 0; i < handlers.Length; i++)
{
Action handler;
lock (this)
{
currentHandler = handler = (Action)handlers[i];
waiting = null;
}
if (handler != null)
try
{
handler();
}
finally
{
currentHandler = null;
lock (this)
if (waiting == handler)
Monitor.Pulse(this);
}
}
}
public void RemoveHandler(Action handler)
{
lock (this)
{
if (Thread.CurrentThread != ownerThread && handler == currentHandler)
{
waiting = handler;
Monitor.Wait(this);
}
var index = Array.IndexOf(handlers, handler);
if (index != -1)
handlers[index] = null;
}
}
}
}
private static readonly object _exceptionLock = new object();
private static EventHandler<EventArgs> _someEvent = delegate { };
public static event EventHandler<EventArgs> SomeEvent
{
add
{
if (value == null)
return;
RuntimeHelpers.PrepareContractedDelegate(value);
lock (_exceptionLock)
_someEvent += value;
}
remove
{
lock (_exceptionLock)
_someEvent -= value;
}
}
RuntimeHelpers.PrepareContractedDelegate(value);
RuntimeHelpers.PrepareDelegate(value);
static void LockFreeUpdate<T> (ref T field, Func <T, T> updateFunction)
where T : class
{
var spinWait = new SpinWait();
while (true)
{
T snapshot1 = field;
T calc = updateFunction (snapshot1);
T snapshot2 = Interlocked.CompareExchange (ref field, calc, snapshot1);
if (snapshot1 == snapshot2) return;
spinWait.SpinOnce();
}
}
Here’s how we can use this method to write a thread-safe event without locks (this is, in fact, what the C# 4.0 compiler now does by default with events):
EventHandler _someDelegate;
public event EventHandler SomeEvent
{
add { LockFreeUpdate (ref _someDelegate, d => d + value); }
remove { LockFreeUpdate (ref _someDelegate, d => d - value); }
}
public sealed class SafeEventInvoker
{
private HashSet<EventHandler> m_Subscribers = new HashSet<EventHandler>();
protected void RaiseEvent()
{
// Локальная копия требуется по тому, что при использовании итератора
// объект будет заблокирован до завершения вызова всех подписок.
EventHandler[] subscribersCopy;
lock (m_Subscribers)
subscribersCopy = m_Subscribers.ToArray();
// Блокировка на каждый отдельный вызов. При этом если из
// делегата будет вызвана отписка от события дедлока не произойдет т.к.
// Monitor.Enter реентерабелен в контексте потока.
foreach (var subscriber in subscribersCopy)
lock (m_Subscribers)
if (m_Subscribers.Contains(subscriber))
subscriber(this, EventArgs.Empty);
}
public event EventHandler ThreadSafeEvent
{
add
{
if (null != value)
lock (m_Subscribers)
m_Subscribers.Add(value);
}
remove
{
if (null != value)
lock (m_Subscribers)
m_Subscribers.Remove(value);
}
}
}SafeEventInvoker.ThreadSafeEvent -= Handler;Проблема в том, что после вызова Delegate.Invoke уже нельзя изменить Invocation List по мере обхода подписчиков делегата.
Всё равно проблема вызова после отписки и отписки в процессе выполнения сводится к атомарности вызова делегата. Само собой, приведенный код не эталон производительности. Но на вскидку — решающий проблемы гонок, блокировок и отписок.
public struct Event<T>
{
private Wrapper[] wrappers;
public void Subscribe(T subscriber)
{
var w = new Wrapper { subscriber = subscriber };
Wrapper[] old;
do { old = wrappers; } while (Interlocked.CompareExchange(ref wrappers, Add(old, w), old) != old);
}
public void Unsubscribe(T subscriber)
{
Wrapper w = null;
Wrapper[] old = wrappers;
for (var i = 0; i < old.Length; i++)
if (old[i] != null && object.Equals(old[i].subscriber, subscriber))
w = old[i];
if (w == null) return;
do { old = wrappers; } while (Interlocked.CompareExchange(ref wrappers, Del(old, w), old) != old);
w.Close();
}
public void Invoke(Action<T> runner)
{
var old = wrappers;
for (var i = 0; i < old.Length; i++)
{
var w = old[i];
if (w != null && w.StartRun())
try { runner(w.subscriber); }
finally { w.FinishRun(); }
}
}
public void Invoke(Action<T, Action> runner)
{
var old = wrappers;
for (var i = 0; i < old.Length; i++)
{
var w = old[i];
if (w != null && w.StartRun())
runner(w.subscriber, w.FinishRun);
}
}
public void Invoke(Action<Action<Action<T>>> dispather)
{
var old = wrappers;
for (var i = 0; i < old.Length; i++)
{
var w = old[i];
if (w != null && w.StartRun())
dispather(runner =>
{
try { runner(w.subscriber); }
finally { w.FinishRun(); }
});
}
}
private static Wrapper[] Add(Wrapper[] array, Wrapper value)
{
if (array == null) return new[] { value, null, null, null };
for (var i = 0; i < array.Length; i++)
if (array[i] == null && Interlocked.CompareExchange(ref array[i], value, null) == null)
return array;
var oldlen = array.Length;
Array.Resize(ref array, oldlen + (oldlen + 1) / 2);
array[oldlen] = value;
return array;
}
private static Wrapper[] Del(Wrapper[] array, Wrapper value)
{
if (array == null) return null;
for (var i = 0; i < array.Length; i++)
if (array[i] == value)
array[i] = null;
return array;
}
private class Wrapper
{
public T subscriber;
public int state = 0;
public bool StartRun()
{
int old;
do { old = state; } while (old >= 0 && Interlocked.CompareExchange(ref state, old + 1, old) != old);
return old >= 0;
}
public void FinishRun()
{
int old;
do { old = state; } while (Interlocked.CompareExchange(ref state, old >= 0 ? old - 1 : old + 1, old) != old);
if (old < -1) lock (this) Monitor.PulseAll(this);
}
public void Close()
{
int old;
do { old = state; } while (old >= 0 && Interlocked.CompareExchange(ref state, ~old, old) != old);
if (old == -1 || old == 0) return;
lock (this) if (state < -1) Monitor.Wait(this);
}
}
}
Потокобезопасные события в C# или Джон Скит против Джеффри Рихтера