UPDATE
As of C# 6, the answer to this question is:
SomeEvent?.Invoke(this, e);
I frequently hear/read the fo
Thanks for a useful discussion. I was working on this problem recently and made the following class which is a bit slower, but allows to avoid callings to disposed objects.
The main point here is that invocation list can be modified even event is raised.
///
/// Thread safe event invoker
///
public sealed class ThreadSafeEventInvoker
{
///
/// Dictionary of delegates
///
readonly ConcurrentDictionary delegates = new ConcurrentDictionary();
///
/// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
/// modification inside of it
///
readonly LinkedList delegatesList = new LinkedList();
///
/// locker for delegates list
///
private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();
///
/// Add delegate to list
///
///
public void Add(Delegate value)
{
var holder = new DelegateHolder(value);
if (!delegates.TryAdd(value, holder)) return;
listLocker.EnterWriteLock();
delegatesList.AddLast(holder);
listLocker.ExitWriteLock();
}
///
/// Remove delegate from list
///
///
public void Remove(Delegate value)
{
DelegateHolder holder;
if (!delegates.TryRemove(value, out holder)) return;
Monitor.Enter(holder);
holder.IsDeleted = true;
Monitor.Exit(holder);
}
///
/// Raise an event
///
///
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;
}
}
///
/// helper class
///
class DelegateHolder
{
///
/// delegate to call
///
public Delegate Action { get; private set; }
///
/// flag shows if this delegate removed from list of calls
///
public bool IsDeleted { get; set; }
///
/// flag shows if this instance was removed from all lists
///
public bool IsDeletedFromList { get; set; }
///
/// Constuctor
///
///
public DelegateHolder(Delegate d)
{
Action = d;
}
}
}
And the usage is:
private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
public event Action SomeEvent
{
add { someEventWrapper.Add(value); }
remove { someEventWrapper.Remove(value); }
}
public void RaiseSomeEvent()
{
someEventWrapper.Raise();
}
Test
I tested it in the following manner. I have a thread which creates and destroys objects like this:
var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());
In a Bar
(a listener object) constructor I subscribe to SomeEvent
(which is implemented as shown above) and unsubscribe in 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;
}
Also I have couple of threads which raise event in a loop.
All these actions are performed simultaneously: many listeners are created and destroyed and event is being fired at the same time.
If there were a race conditions I should see a message in a console, but it is empty. But if I use clr events as usual I see it full of warning messages. So, I can conclude that it is possible to implement a thread safe events in c#.
What do you think?