GOTCHA 64 Raising events lacks thread-safety


GOTCHA #64 Raising events lacks thread-safety

When you are about to raise an event, what happens if all handlers for that event are suddenly removed by other threads? What you don't want is for your program to crash with a NullReferenceException, which it may well do if you aren't careful. In Gotcha #7, "Uninitialized event handlers aren't treated gracefully," I discussed the issues with raising events. At the end of that gotcha I raised a concern about thread safety. In this gotcha I address that.

Unfortunately, this problem manifests itself slightly differently in C# than it does in VB.NET. First, I'll discuss the problem with raising events in VB.NET. Then I'll discuss the same problem in C#. The discussion regarding VB.NET is relevant for C# programmers as I build on it.

First, RaiseEvent() in VB.NET is not thread-safe. Consider the example in Example 7-37.

Example 7-37. Example of RaiseEvent in VB.NET

VB.NET (RaisingEventThreadSafety)

 Imports System.Threading Public Class AComponent     Public Event myEvent As EventHandler     Private Sub DoWork()         Thread.Sleep(5) ' Simulate some work delay         RaiseEvent myEvent(Me, New EventArgs)     End Sub     Public Sub Work()         Dim aThread As New Thread(AddressOf DoWork)         aThread.Start()     End Sub End Class 'Test.vb Imports System.Threading Module Test     Private Sub TestHandler(ByVal sender As Object, ByVal e As EventArgs)         Console.WriteLine("TestHandler called")     End Sub     Sub Main()         Dim obj As New AComponent         AddHandler obj.myEvent, AddressOf TestHandler         obj.Work()         Thread.Sleep(5)         RemoveHandler obj.myEvent, AddressOf TestHandler     End Sub End Module 

In the Work() method of the AComponent class, you call the DoWork() method in a separate thread. In the DoWork() method you raise an event after a small delay (5ms). In the Main() method of the Test class you create an instance of AComponent, register a handler with it, then call its Work() method. You then quickly (after a delay of 5 ms) remove the handler you registered. A quick run of this program produces the output in Figure 7-32.

Figure 7-32. Output from AComponent.vb


It worked this time, but you just got luckylucky that the thread executing DoWork() got to its RaiseEvent() before Main() got to its RemoveHandler(). With the code the way it is, though, there's no guarantee things will always turn out so well.

There's good news and there's bad news. First, let's look at the good news. AddHandler and RemoveHandler are thread-safe. The VB.NET compiler translates them into calls to special hidden thread-safe add and remove methods (add_myEvent() and remove_myEvent() in this case). You can verify this by observing the MSIL code for Example 7-37 in Figure 7-33.

Notice that AddHandler() is translated to a call to the add_myEvent() method. add_myEvent()is marked synchronized (as you can see at the top of Figure 7-34). What does that mean? When add_myEvent() is called, it gains a lock on the AComponent instance that myEvent belongs to. Within the method, the registration of the handler (by the call to the Combine() method on the delegate) is done with thread safety. The code for remove_myEvent() method is similarly thread-safe. You can see this by examining the MSIL (not shown here) using ildasm.exe.

Figure 7-33. MSIL for AddHandler and RemoveHandler calls in Example 7-37


Figure 7-34. Auto-generated hidden special thread-safe add_myEvent() method


Unfortunately, RaiseEvent is not thread-safe, as you can see from the MSIL in Figure 7-35.

Figure 7-35. MSIL for RaiseEvent that shows lack of thread-safety


The event field is first loaded and verified to be not Nothing. If the test succeeds, then the field is loaded again, and the Invoke() method is called. Here is the gotcha: what if the field is set to null/Nothing between the check and the reload of the field? Tough luck. Can this happen? You bet it can.

I wanted to go to extreme measures to prove to myself that this is true. So, I got into the Visual Studio debugger, asked for Disassembly, and started stepping through the assembly. By doing so, I slowed down the execution of the RaiseEvent statement, allowing the Main() method to modify the event field before RaiseEvent completes. I had no trouble producing the NullReferenceException shown in Figure 7-36

Figure 7-36. RaiseEvent failing due to lack of thread-safety


How can you avoid this problem? There is no good answer. The simplest solution that comes to mind is to surround the RaiseEvent() call with a SyncLock block:

     SyncLock Me         RaiseEvent myEvent(Me, New EventArgs)     End SyncLock 

While this certainly provides thread safety from concurrent AddHandler and RemoveHandler calls, it may also delay the registration and unregistration of handlers if the event-handler list is large. It would be nice if you could get the list of delegates from the event. Unfortunately, the VB.NET compiler does not allow you to access the underlying delegate (the C# compiler does).

One possibility is to use delegates directly instead of using an event. This, however, introduces some complications as well. Unlike registering and unregistering for events, registering and unregistering handlers for delegates is not thread-safe; you have to deal with thread safety yourself if you go this route.

Let's look at how this is similar and how this differs in C#. In C#, you use the += operator to register a handler for an event, and -= to unregister. These two calls become calls to the hidden special add and remove methods, as discussed above for VB.NET. Providing thread safety while raising events in C# is pretty easy. The code in Example 7-38 shows how to do that.

Example 7-38. C# example to raise event with thread-safety

C# (RaisingEventThreadSafety)

 //AComponent.cs using System; using System.Threading; namespace RaisingEvent {     public class AComponent     {         public event EventHandler myEvent;         private void DoWork()         {             EventHandler localHandler = null;             lock(this)             {                 if (myEvent != null)                 {                     Console.WriteLine("myEvent is not null");                     Thread.Sleep(2000);                     // Intentional delay to illustrate                     localHandler = myEvent;                     Console.WriteLine("Got a safe copy");                 }             }             if (localHandler != null)             {                 localHandler(this, new EventArgs());             }             else             {                 Console.WriteLine("localHandler is null!!!!!!!!!!!");             }         }         public void Work()         {             Thread aThread = new Thread(                 new ThreadStart(DoWork));             aThread.Start();         }     } } //Test.cs using System; using System.Threading; namespace RaisingEvent {     public class Test     {         private static void TestHandler(             object sender, EventArgs e)         {             Console.WriteLine("Handler called");         }         public static void TestIt()         {             AComponent obj = new AComponent();             obj.myEvent += new EventHandler(TestHandler);             obj.Work();             Thread.Sleep(1000);             Console.WriteLine("Trying to unregister handler");             obj.myEvent -= new EventHandler(TestHandler);             Console.WriteLine("handler unregistered");         }         public static void Main()         {             TestIt();         }     } } 

The output from this example is shown in Figure 7-37. You can see that the remove-handler code (the -= operator) in the TestIt() method blocks while a copy of the reference to myEvent is made in DoWork().

Figure 7-37. Output from Example 7-38 illustrates thread-safety


Well, that looks good. Are you done? I wish. For its share, C# has made a mess with the thread safety of events when the registration and unregistration of handlers is done inside the class owning the event. But first, let's quickly take a look at the MSIL for the TestIt() method as shown in Figure 7-38.

Figure 7-38. MSIL for the Test.TestIt() method in Example 7-38


As you can see, the call to += in the source code of Testit() method uses the thread-safe add_myEvent() method at the MSIL level. This is good news.

Now, copy the TestIt() method and the TestHandler() method from the Test class to the AComponent class, compile the code and take a look at the MSIL (shown in Figure 7-39).

Figure 7-39. MSIL for AComponent.Testit() method


Unfortunately, this version uses the Combine() method directly instead of the thread-safe add_myEvent() method. Let's make a small change to Main() as shown below. The output after this change is shown in Figure 7-40.

Figure 7-40. Output after the above change to Main()


         public static void Main()         {             Console.WriteLine("---- Calling Main.TestIt ----");             TestIt();             Thread.Sleep(5000);             Console.WriteLine("---- Calling AComponent.TestIt ----");             AComponent.TestIt();         } 

You would expect the result of the calls to Test.TestIt() and AComponent.TestIt() to be identical, but they're not. This is because event registration and unregistration are not thread-safe within the class that contains the event.

While this gotcha is a problem in C#, it does not exist in VB.NET. AddHandler() and RemoveHandler() are consistently thread-safe no matter where they're called from.

IN A NUTSHELL

  • Calls to register and unregister events are thread-safe in VB.NET.

  • In C#, they are thread-safe only if called from outside the class with the event. For calls within the class, you must provide thread safety yourself by calling += or -= within a lock(this) statement.

  • The thread-safe calls to register and unregister events rely on the special hidden thread-safe add and remove methods, respectively.

  • Calls to RaiseEvent in VB.NET are inherently not thread-safe. You need to take care of thread-safety for these calls.

  • You can use the underlying delegate to raise the event in a thread-safe manner in C#. You achieve this by getting a local reference to the delegate within a lock(this) statement and then using the local reference (if not null) to raise the event.

SEE ALSO

Gotcha #7, "Uninitialized event handlers aren't treated gracefully," Gotcha #56, "Calling Type.GetType() may not return what you expect," Gotcha #57, "Locking on globally visible objects is too sweeping," and Gotcha #62, "Accessing WinForm controls from arbitrary threads is dangerous."



    .NET Gotachas
    .NET Gotachas
    ISBN: N/A
    EAN: N/A
    Year: 2005
    Pages: 126

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net