An area I’ve noticed where engineers often forget to think about synchronization is where firing events.
Now I’m going to go over a little background on C# delegates quickly just to refresh what we learnt or should have learnt years ago at the beginnings of the C# language.
It seems to be a common misconception, that all that is needed to keep synchronisation,
is to check the delegate (technically a MulticastDelegate, or in architectural terms the publisher of the publish-subscribe pattern (more commonly known as the observer pattern)) for null.
Defining the publisher without using the event keyword
public class Publisher { // ... // Define the delegate data type public delegate void MyDelegateType(); // Define the event publisher public MyDelegateType OnStateChange { get{ return _onStateChange;} set{ _onStateChange = value;} } private MyDelegateType _onStateChange; // ... }
When you declare a delegate, you are actually declaring a MulticastDelegate.
The delegate keyword is an alias for a type derived from System.MulticastDelegate.
When you create a delegate, the compiler automatically employs the System.MulticastDelegate type rather than the System.Delegate type.
When you add a method to a multicast delegate, the MulticastDelegate class creates a new instance of the delegate type, stores the object reference and the method pointer for the added method into the new instance, and adds the new delegate instance as the next item in a list of delegate instances.
Essentially, the MulticastDelegate keeps a linked list of Delegate objects.
It’s possible to assign new subscribers to delegate instances, replacing existing subscribers with new subscribers by using the = operator.
Most of the time what is intended is actually the += operator (implemented internally by using System.Delegate.Combine()).
System.Delegate.Remove() is what’s used when you use the -+ operator on a delegate.
class Program { public static void Main() { Publisher publisher = new Publisher(); Subscriber1 subscriber1 = new Subscriber1(); Subscripber2 subscripber2 = new Subscripber2(); publisher.OnStateChange = subscriber1.OnStateChanged; // Bug: assignment operator overrides previous assignment. // if using the event keyword, the assignment operator is not supported for objects outside of the containing class. publisher.OnStateChange = subscriber2.OnStateChanged; } }
Another short coming of the delegate is that delegate instances are able to be invoked outside of the containing class.
class Program { public static void Main() { Publisher publisher = new Publisher(); Subscriber1 subscriber1 = new Subscriber1(); Subscriber2 subscriber2 = new Subscriber2(); publisher.OnStateChange += subscriber1.OnStateChanged; publisher.OnStateChange += subscriber2.OnStateChanged; // lack of encapsulation publisher.OnStateChange(); } }
C# Events come to the rescue
in the form of the event keyword.
The event keyword address’s the above problems.
The modified Publisher looks like the following:
public class Publisher { // ... // Define the delegate data type public delegate void MyDelegateType(); // Define the event publisher public event MyDelegateType OnStateChange; // ... }
Now. On to synchronisation
The following is an example from the GoF guys with some small modifications I added.
You’ll also notice, that the above inadequacies are taken care of.
Now if the Stock.OnChange is not accessed by multiple threads, this code is fine.
If it is accessed by multiple threads, it’s not fine.
Why I hear you ask?
Well, between the time the null check is performed on the Change event
and when Change is fired, Change could be set to null, by another thread.
This will of course produce a NullReferenceException.
The code on lines 59,60 is not atomic.
using System; using System.Collections.Generic; namespace DoFactory.GangOfFour.Observer.NETOptimized { /// <summary> /// MainApp startup class for .NET optimized /// Observer Design Pattern. /// </summary> class MainApp { /// <summary> /// Entry point into console application. /// </summary> static void Main() { // Create IBM stock and attach investors var ibm = new IBM(120.00); // Attach 'listeners', i.e. Investors ibm.Attach(new Investor { Name = "Sorros" }); ibm.Attach(new Investor { Name = "Berkshire" }); // Fluctuating prices will notify listening investors ibm.Price = 120.10; ibm.Price = 121.00; ibm.Price = 120.50; ibm.Price = 120.75; // Wait for user Console.ReadKey(); } } // Custom event arguments public class ChangeEventArgs : EventArgs { // Gets or sets symbol public string Symbol { get; set; } // Gets or sets price public double Price { get; set; } } /// <summary> /// The 'Subject' abstract class /// </summary> abstract class Stock { protected string _symbol; protected double _price; // Constructor public Stock(string symbol, double price) { this._symbol = symbol; this._price = price; } // Event public event EventHandler<ChangeEventArgs> Change; // Invoke the Change event private void OnChange(ChangeEventArgs e) { // not thread safe if (Change != null) Change(this, e); } public void Attach(IInvestor investor) { Change += investor.Update; } public void Detach(IInvestor investor) { Change -= investor.Update; } // Gets or sets the price public double Price { get { return _price; } set { if (_price != value) { _price = value; OnChange(new ChangeEventArgs { Symbol = _symbol, Price = _price }); Console.WriteLine(""); } } } } /// <summary> /// The 'ConcreteSubject' class /// </summary> class IBM : Stock { // Constructor - symbol for IBM is always same public IBM(double price) : base("IBM", price) { } } /// <summary> /// The 'Observer' interface /// </summary> interface IInvestor { void Update(object sender, ChangeEventArgs e); } /// <summary> /// The 'ConcreteObserver' class /// </summary> class Investor : IInvestor { // Gets or sets the investor name public string Name { get; set; } // Gets or sets the stock public Stock Stock { get; set; } public void Update(object sender, ChangeEventArgs e) { Console.WriteLine("Notified {0} of {1}'s " + "change to {2:C}", Name, e.Symbol, e.Price); } } }
At least we don’t have to worry about the += and -= operators. They are thread safe.
Ok. So how do we make it thread safe?
Now I’ll do my best not to make your brain hurt.
We can assign a local copy of the event and then check that instead.
How does that work you say?
The Change delegate is a reference type.
You may think that threadSafeChange references the same location as Change,
thus any changes to Change would also be reflected in threadSafeChange.
That’s not the case though.
Change += investor.Update does not add a new delegate to Change, but assigns it a new MulticastDelegate,
which has no effect on the original MulticastDelegate that threadSafeChange also references.
The reference part of reference type local variables is stored on the stack.
A new stack frame is created for each thread with every method call
(whether its an instance or static method).
All local variables are safe…
so long as they are not reference types being passed to another thread or being passed to another thread by ref.
So, only one thread can access the threadSafeChange instance.
private void OnChange(ChangeEventArgs e) { // assign reference to heap allocated memory to stack allocated implements thread safety EventHandler<ChangeEventArgs> threadSafeChange = Change; if ( threadSafeChange != null) threadSafeChange(this, e); }
Now for a bit of error handling
If one subscriber throws an exception, any subscribers later in the chain do not receive the publication.
One way to get around this problem, is to semantically override the enumeration of the subscribers.
Thus providing the error handling.
private void OnChange(ChangeEventArgs e) { // assign reference to heap allocated memory to stack allocated implements thread safety EventHandler<ChangeEventArgs> threadSafeChange = Change; if ( threadSafeChange != null) { foreach(EventHandler<ChangeEventArgs> handler in Change.GetInvocationList()) { try { //if subscribers delegate methods throw an exception, we'll handle in the catch and carry on with the next delegate handler(this, e); // if we only want to allow a single subscriber if (Change.GetInvocationList().Length > 1) throw new Exception("Too many subscriptions to the Stock.Change" /*, provide a meaningful inner exception*/); } catch (Exception exception) { // what we do here depends on what stage of development we are in. // if we're in early stages, pre-release, fail early and hard. } } } }