原文(C# Multithreading and Events):http://www.codeproject.com/Articles/886223/Csharp-Multithreading-and-Events
First let’s start off with there is no perfectly safe multithreaded event handling as there are caveats/conditions that must be handled. So it’s really if the particular method you plan to use is safe for the uses you require.
There is a great article here about this:
http://www.codeproject.com/Articles/37474/Threadsafe-Events
The one suggested solution the author mentions is don’t use events in a multithreaded way. (This can be a good solution when possible.)
I’m going to show there is another problem to contend with and I will offer some classes to help handle some scenarios plus propose some alternative options you could use, but it depends on if your events can be used that way. Ultimately it depends on what you need to do, and implementation/use and the complexity you wish to manage.
To start let’s define the types of multithreading problems that come up with trying to solve this problem:
The general consensus is there is no way to solve all of these issues. Perhaps but maybe let’s see the ways we can work with these issues.
This should not be a problem in recent versions of .net 4.5 is reportedly using lockfree constructs that should avoid this type of issue. If you need to handle this in earlier versions you could implement your own add/remove handlers with your own lock object to avoid some potential for deadlock scenario’s.
The problem is an even by default is null until you add a handler to it. If one thread is trying to add as another is trying to raise the event, or the last handler is trying to unsubscribe, when you try to call the event you could get a null reference exception.
In this scenario one thread adds a handler, but an event gets raised in the middle and this handler doesn’t appear to be immediately called.
So either controlling your event source is needed, not needing to worry much about a missed event, or you would need something more like a message bus that replayed events that already happened before subscribing.
In this scenario one thread removes a handler but an event is raised on another thread while this is happening and it still gets called even after it was thought to have unsubscribed.
In this scenario it causes a deadlock because if the event mechanism holds a lock while subscribing or unsubscribing and while calling events it might be possible to deadlock. (See this article for more detailshttp://www.codeproject.com/Articles/37474/Threadsafe-Events)
We will see in the recommended solution that first the code tries to take a copy of the delegate event list to fire. When it does this it’s possible it might not get the most recent list because that another thread already updated the list. This might not be that big of an issue it is similar to problem #3 in that the net result is an added event subscription might not receive a notification immediately after subscribing
NOTE: This is more of an issue with the publisher of events controlling event ordering properly.
This can happen if multiple threads fire events at the same time, its possible they can appear to the subscriber out of the expected sequence if there was an expected sequence. Once you have decided that multiple threads can fire events this could happen at anytime even with proper locking, one thread beats another to firing an event so it appears first. A slight variation #7(B) of this is where the list of delegates starts to get one event sent to the list and then gets another event sent to it even before the first event has completed firing to the full list. If you really wanted multithreaded events you might consider this a feature to truly support multithreading. Many events do have a typical order. Such as Got focus, Lost focus, Session start, Session end (either indicated with EventArgs in your event or via separate events for each of those). The safest way to avoid this is to always fire your events from one thread if this matters and/or control the controller that decides on firing events with multithreaded synchronization to avoid it sourcing multiple events at the same time. So this problem will exist in any multithreaded scenario where that is not done. Since those are actions need to be controlled by the publisher likely with some type of state management that is not covered here.
(At the end of the article an example is shown of how to address event ordering between two events where the sequence matters)
We can exclude problem #1 since as mentioned it is not really an issue anymore. Problem #3 as mentioned almost all implementations will have unless you control your source of events/timing or implement your events like a message bus to send new subscribes all of the previous events. .NET events typically aren’t used that way.
public ThisEventHandler ThisEvent; protected virtual OnThisEvent (ThisEventArgs args) { If (ThisEvent!=null) ThisEvent (this,args); // ThisEvent could be null so you could get a null reference exception }
Problem | |
#2 Null exception race condition | X |
#3 A new subscriber might not get called immediately | X |
#4 A handler is still called after unsubscribe. | X |
#5 Possible deadlock | |
#6 Might not get the most recent list of delegates to fire | |
#7 Problem/Feature - Events could be fired to delegates out of sequence | X |
Let’s start with the recommended solution to understand what it handles and what it doesn’t.
public ThisEventHandler ThisEvent; protected virtual void OnThisEvent (ThisEventArgs args) { ThisEventHandler thisEvent=ThisEvent; //assign the event to a local variable If (thisEvent!=null) { thisEvent (this,args); } }
This solves problem #2 and does not have problem #5
This has problems: #3 and #4. Which is that if you add a new handler it might not get called immediately and if you remove a handler it may still get called for a short time. Better at least, we got rid of one problem. We also see this solution introduces a possible #6 issue, it takes a copy of the list of handlers in the local thisEvent variable but because this variable is not volatile it may not get the most recent copy of the handler list.
Problem | |
#2 Null exception race condition | |
#3 A new subscriber might not get called immediately | X |
#4 A handler is still called after unsubscribe. | X |
#5 Possible deadlock | |
#6 might not get most recent copy of handlers to call | X |
#7 Problem/Feature - Events could be fired to delegates out of sequence | X |
One way you can workaround problem #3 (adding a new handler – it doesn’t get called immediately) (if possible) is to add your handlers early before it is likely that multiple threads will be calling your event. This avoids issues with #3.
For #4 when you are unsubscribing set a bool flag that your event handler can check to see if it is being unsubscribed. If it is do nothing. Or just have smart handling in your event handler to know when this has happened. The problem is there is no safe way to do this without locking.
Let’s see if we can do better:
In the event publisher if we use this code:
volatile ThisEventHandler _localEvent = null; //need volatile to ensure we get the latest public virtual void OnThisEvent (EventArgs args) { _localEvent=ThisEvent; //assign the event to a local variable – notice is volatile class field if (_localEvent!=null) { _localEvent(this,args); } }
When not using locks in the event publisher, notice the variable _localEvent many examples you might see on the internet save a copy of the event into a variable and then call it if not null. They typically don’t use a class volatile field. They should otherwise they might not be getting the latest copy. The net effect may not be that much of an issue using a class field vs a local variable as it would generally be expected when a method is called that the local variable will be initialized at least once. In C# local variables can’t be volatile though, so to ensure reads/writes are directed to memory a volatile class field can be used.
In the event subscriber class if we use this:
private object _lock = new object(); private volatile bool _unsubscribed=false; private void MethodToUnsubscribe() { lock(_lock) { _unsubscribed = true; ThisEvent -= new ThisEventHandler(ThisEventChanged); } } public void Subscribe() { lock(_lock) { _unsubscribed = false; ThisEvent += new ThisEventHandler(ThisEventChanged); } } //Event handler private void ThisEventChanged(object sender, ThisEventArgs e) { lock(_lock) { if (!_unsubscribed) { //Do work here } } }
In the event subscriber class if we use this:
private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); private volatile bool _unsubscribed=false; private void MethodToUnsubscribe() { _rwLock.EnterWriteLock(); _unsubscribed = true; ThisEvent -= new ThisEventHandler(ThisEventChanged); _rwLock.ExitWriteLock(); } public void Subscribe() { _rwLock.EnterWriteLock(); _unsubscribed = false; ThisEvent += new ThisEventHandler(ThisEventChanged); _rwLock.ExitWriteLock(); } //Event handler – due to read lock could get called out of sequence by multiple threads private void ThisEventChanged(object sender, ThisEventArgs e) { _rwLock.EnterReadLock(); try { if (!_unsubscribed) { //Do work here } } finally { _rwLock.ExitReadLock(); } }
This unfortunately puts some burden on the subscriber of the event.
IMPORTANT: This example code also allows for multiple threads to fire calls at the same time, which might not be desired. If not desired just use a regular lock instead of the reader/writer lock otherwise your events could come out of sequence.
If you implement the publisher and subscriber code:
Problem | |
#2 Null exception race condition | |
#3 A new subscriber might not get called immediately | |
#4 A handler is still called after unsubscribe. | |
#5 Possible deadlock | X |
#6 might not get most recent copy of handlers to call | |
#7 Problem/Feature - Events could be fired to delegates out of sequence | X |
If your events can be handled this way then some of the above issues are either solved or worked around. If your code can not handle the limitations of this method there are other implementations but they will suffer from one or more of the problems listed. So it will come down to your code requirements and implementation complexity about how you wish to handle multithreaded events. (Or potentially as the first article mentions, don’t do that and only use events from a single thread).
If you implement just the event publisher code and not the subscriber code:
Problem | |
#2 Null exception race condition | |
#3 A new subscriber might not get called immediately | X |
#4 A handler is still called after unsubscribe. | X |
#5 Possible deadlock | X |
#6 might not get most recent copy of handlers to call | |
#7 Problem/Feature - Events could be fired to delegates out of sequence | X |
Both solutions solved the problem #6 shown in the standard recommended solution by using a volatile field in the publisher code to ensure the most recent copy of the event handlers is copied.
The above method shows how to handle this problem in the code that uses an event rather than in the code that publishes the event.
Let’s look at an example that wraps up the logic to handle events a bit more safely:
Let’s look at wrapper class implementation that puts this all together so that the implementer of an event can handle most of the issues to not burden the listeners/susbcribers. The below code is an implementation that can be used for event publishers. This implementation supports multithreaded non-sequential events being fired.
/// <summary> /// SafeHandlerInfo - used for storing handler info, plus contains a flag /// indicating if subscribed or not and the reader writer lock /// for when the subscription is read or updated. /// </summary> /// <typeparam name="TArgs">Event args</typeparam> internal class SafeHandlerInfo< TArgs> where TArgs : EventArgs { public EventHandler<TArgs> Handler; public bool Subscribed = true; //public object LockObj = new object(); public ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); public SafeHandlerInfo(EventHandler<TArgs> handler) { Handler = handler; } } /// <summary> /// <summary> /// SafeEvent class provides safety for unsubscribing from events so even with /// multiple threads after unsubscribing it will not get called. /// Also makes sure that a null exception won't happen due to the removing of /// events. Only one event is fired at a time, so single threaded through this event. /// </summary> /// <typeparam name="TArgs">The type of the event args</typeparam> public class SafeEvent<TArgs> where TArgs : EventArgs { private object _lock = new object(); private event EventHandler<TArgs> _event; public event EventHandler<TArgs> Event { add { Subscribe(value); } remove { Unsubscribe(value); } } /// <summary> /// Used to fire this event from within the class using the SafeEvent /// /// </summary> /// <param name="args">The event args</param> public virtual void FireEvent(object sender,TArgs args) { lock (_lock) { EventHandler<TArgs> localEvents = _event; if (localEvents != null) localEvents(sender, args); } } /// <summary> /// Unsubscribe - internally used to unsubscribe a handler from the event /// </summary> /// <param name="unsubscribeHandler">The handler being unsubscribed</param> protected void Unsubscribe(EventHandler<TArgs> unsubscribeHandler) { lock (_lock) { _event -= unsubscribeHandler; } } /// <summary> /// Subscribe - Called to subscribe the handler /// </summary> /// <param name="eventHandler">The handler to subscribe</param> protected void Subscribe(EventHandler<TArgs> eventHandler) { lock (_lock) { _event += eventHandler; } } }
The above code solves similar problems from the publishing side.
Problems | |
#2 Null exception race condition | |
#3 A new subscriber might not get called immediately | |
#4 A handler is still called after unsubscribe. | |
#5 Possible deadlock | X |
#6 might not get most recent copy of handlers to call | |
#7 Problem/Feature - Events could be fired to delegates out of sequence | X |
This code is susceptible to deadlocks under certain scenario’s
Potential Deadlock - If they unsubscribe an existing handler of the event currently being fired there could be the potential for deadlock. So the best rule to follow is don’t try to unsubscribe while in the middle of the event being called and avoid that in any downstream functions that are called by that event.
This hopefully should be very controllable because as the subscriber of the event you should know when you want to unsubscribe. This would only happen if a separate thread then the one being fired cause the unsubscribe of the current event being fired. So fairly limited chance.
Next let’s have an example of how to use the SafeEvent class:
/// <summary> /// TestEventUser /// Test the SafeEvent class for implementing an event using SafeEvent /// </summary> public class TestEventUse { //declare our private event private SafeEvent<EventArgs> _myEvent = new SafeEvent<EventArgs>(); /// <summary> /// MyEvent - proxy the add/remove calls to the private event /// </summary> public event EventHandler<EventArgs> MyEvent { add { _myEvent.Event += value; } remove { _myEvent.Event -= value; } } /// <summary> /// OnMyEvent - standard example idiom of how to fire an event /// </summary> /// <param name="args"></param> protected void OnMyEvent(EventArgs args) { _myEvent.FireEvent(this,args); //call our private event to fire it } /// <summary> /// FireEvent - This we provided on our test class as a quick way to fire the event from another class. /// </summary> public void FireEvent() { OnMyEvent(new EventArgs()); } }
Most implementations of event handling you’ll see around either have many of the multithreading issues mentioned or will allow only a single thread in to fire a single event from a single thread at a time.
SafeEventsNS will allow multiple threads to fire the event at the same time and the subscriber can get multithreaded calls at the same time. This means that notifications might not come in sequence. This is not the normal way most events are handled, most event handlers probably care about the sequence of events that come in. But if you don’t and you need a relatively safe way to fire an event from multiple threads to multiple subscribers at the same time, this class might be useful.
/// <summary> /// SafeHandlerInfo - used for storing handler info, plus contains a flag /// indicating if subscribed or not and the reader writer lock /// for when the subscription is read or updated. /// </summary> /// <typeparam name="TArgs">Event args</typeparam> internal class SafeHandlerInfo< TArgs> where TArgs : EventArgs { public EventHandler<TArgs> Handler; public bool Subscribed = true; //public object LockObj = new object(); public ReaderWriterLockSlim Lock = new ReaderWriterLockSlim(); public SafeHandlerInfo(EventHandler<TArgs> handler) { Handler = handler; } } /// <summary> /// SafeEvent class provides safety for unsubscribing from events /// so even with multiple threads after unsubscribing it will not get called. /// Also makes sure that a null exception won't happen due to the removing of events /// </summary> /// <typeparam name="TArgs">The type of the event args</typeparam> public class SafeEventNS<TArgs> where TArgs : EventArgs { Dictionary<EventHandler<TArgs>, SafeHandlerInfo<TArgs>> _handlers = new Dictionary<EventHandler<TArgs>, SafeHandlerInfo<TArgs>>(); private ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim(); public event EventHandler<TArgs> Event { add { Subscribe(value); } remove { Unsubscribe(value); } } /// <summary> /// Used to fire this event from within the class using the SafeEvent /// This reads a copy of the list and calls all of the subscribers in it that are still subscribed. /// Anything that was unsubscribed gets removed at the end of the event call. It was handler here since /// The copy of the list might be held by multiple threads unsubscribe flags a handler unsubscribed and removes it. /// that way if it is still in the list it will not be called /// /// </summary> /// <param name="args">The event args</param> public virtual void FireEvent(object sender,TArgs args) { _rwLock.EnterReadLock(); List<SafeHandlerInfo<TArgs>> localHandlerInfos = _handlers.Values.ToList(); _rwLock.ExitReadLock(); foreach (SafeHandlerInfo<TArgs> info in localHandlerInfos) { info.Lock.EnterReadLock(); try { if (info.Subscribed) { EventHandler<TArgs> handler = info.Handler; try { handler(sender, args); } catch { }; } } finally { info.Lock.ExitReadLock(); } } } /// <summary> /// Unsubscribe - internally used to unsubscribe a handler from the event /// </summary> /// <param name="unsubscribeHandler">The handler being unsubscribed</param> protected void Unsubscribe(EventHandler<TArgs> unsubscribeHandler) { _rwLock.EnterWriteLock(); try { SafeHandlerInfo<TArgs> handler = null; if (_handlers.TryGetValue(unsubscribeHandler, out handler)) { handler.Lock.EnterWriteLock(); try { handler.Subscribed = false; _handlers.Remove(handler.Handler); } finally { handler.Lock.ExitWriteLock(); } } } catch (Exception e) { throw e; } finally { _rwLock.ExitWriteLock(); } } /// <summary> /// Subscribe - Called to subscribe the handler /// </summary> /// <param name="eventHandler">The handler to subscribe</param> protected void Subscribe(EventHandler<TArgs> eventHandler) { _rwLock.EnterWriteLock(); try { SafeHandlerInfo<TArgs> handlerInfo = null; if (!_handlers.TryGetValue(eventHandler, out handlerInfo)) { handlerInfo = new SafeHandlerInfo<TArgs>(eventHandler); handlerInfo.Lock.EnterWriteLock(); try { _handlers.Add(eventHandler, handlerInfo); } finally { handlerInfo.Lock.ExitWriteLock(); } } else { handlerInfo.Lock.EnterWriteLock(); try { handlerInfo.Subscribed = true; } finally { handlerInfo.Lock.ExitWriteLock(); } } } catch (Exception e) { throw e; } finally { _rwLock.ExitWriteLock(); } } }
Let’s show an example of how to use this, which is the same way as the previous SafeEvent class.
/// <summary> /// TestEventUser /// Test the SafeEvent class for implementing an event using SafeEvent /// </summary> public class TestSafeEventNS { //declare our private event private SafeEventNS<EventArgs> _myEvent = new SafeEventNS<EventArgs>(); /// <summary> /// MyEvent - proxy the add/remove calls to the private event /// </summary> public event EventHandler<EventArgs> MyEvent { add { _myEvent.Event += value; } remove { _myEvent.Event -= value; } } /// <summary> /// OnMyEvent - standard example idiom of how to fire an event /// </summary> /// <param name="args"></param> protected void OnMyEvent(EventArgs args) { _myEvent.FireEvent(this,args); //call our private event to fire it } /// <summary> /// FireEvent - This we provided on our test class as a quick way to fire the event from another class. /// </summary> public void FireEvent() { OnMyEvent(new EventArgs()); } }
The SafeEventNS class still suffers from the potential for deadlocks, but offers the benefit of true multithreaded event support, if needed.
Problems | |
#2 Null exception race condition | |
#3 A new subscriber might not get called immediately | |
#4 A handler is still called after unsubscribe. | |
#5 Possible deadlock | X |
#6 might not get most recent copy of handlers to call | |
#7 Problem/Feature - Events could be fired to delegates out of sequence | X |
Reminder Problem #7 has to be handled by the publisher of the event only publishing in the correct sequence using multithreaded synchronization.
So as mentioned in the code project article and the original author John Skeet, there is sort of a way to solve many of these problems, which is don’t subscribe/unsubscribe or fire the events from multiple threads, only use one thread. That will avoid the locking overhead and some of the complexity and solves all of the problems (sort of). It does add some complexity on the threading model. Also it is arguable as to if it solved #3 and #4 problems as if subscription is put into a queue to be handled on another thread, then you don’t know when you will finally be subscribed or unsubscribed. If you built an asynchronous notification to when you were subscribed or unsubscribed that might be the one way to try to solve that.
For other potential implementations take a look at the previously mentioned article mentioned it explains the implementations and the problems each of them has:
http://www.codeproject.com/Articles/37474/Threadsafe-Events
As mentioned this problem is when events can be fired out of sequence. To solve that the source of events must be controlled, so if there is a required sequencing of events you protect your state machine and the firing of those events with synchronization if multiple threads could be firing it.
/// <summary> /// TestEventUseSolveProblemSeven /// Test the SafeEvent class for implementing an event using /// SafeEvent – we want StartEvent to be fired before EndEvent /// This shows how to solve problem #7. If FireEvent() is that /// only thing that could be used to fire events then /// that will guarantee a StartEvent is called before an EndEvent() /// </summary> public class TestEventUseSolveProblemSeven { //declare our private event private SafeEvent<EventArgs> _startEvent = new SafeEvent<EventArgs>(); private SafeEvent<EventArgs> _endEvent = new SafeEvent<EventArgs>(); private object _lock = new object(); bool _started = false; /// <summary> /// StartEvent - proxy the add/remove calls to the private event /// </summary> public event EventHandler<EventArgs> StartEvent { add { _startEvent.Event += value; } remove { _startEvent.Event -= value; } } public event EventHandler<EventArgs> EndEvent { add { _endEvent.Event += value; } remove { _endEvent.Event -= value; } } /// <summary> /// OnStartEvent - standard example idiom of how to fire an event /// </summary> /// <param name="args"></param> protected void OnStartEvent(EventArgs args) { _startEvent.FireEvent(this, args); //call our private event to fire it } /// <summary> /// OnEndEvent - standard example idiom of how to fire an event /// </summary> /// <param name="args"></param> protected void OnEndEvent(EventArgs args) { _endEvent.FireEvent(this, args); //call our private event to fire it } /// <summary> /// FireEvent - This we provided on our test class as a quick way to fire /// the event from another class. /// </summary> public void FireEvent() { // by using a lock to prevent other events from being fired and managing our // state we can guarantee event ordering. lock (_lock) { if (_started) OnEndEvent(new EventArgs()); else OnStartEvent(new EventArgs()); _started = !_started; } } }
So as we have seen there is no straightforward easy answer to all multithreading issues. So you will need to pick the best answer for your purpose/requirements and handle the scenarios that can occur properly to avoid event/multithreading issues.