Multithreading is a powerful tool. It is also complex—I have enjoyed many a pizza at the office during lengthy debugging sessions. There are methods and facilities that work well only when used in a certain way. It is like tightrope walking; you can’t afford to slip to either side. Fully understand the capabilities, behavior, and limitations of the API before using it.
The Thread
class provides a number of methods that are useful, a few that are not so useful, and some that are quite dangerous. Also, you have the options of creating a thread yourself, using one from the thread pool, or even using asynchronous features provided by the WinForms
framework. Which one should you choose and why? What are the consequences of choosing one over the other? What happens if an exception is thrown from within a thread? How is this handled? Is it handled at all? What is the lifetime of a thread you create, and how can you control it? What happens when a thread is terminated? What about cleanup of resources? These are all questions you should ask before using the API.
In this chapter, I address these questions and provide answers that will steer you clear of the perils. I discuss general problems with threading, the thread pool, asynchronous calls using delegates, and threading problems related to Windows Forms [Griffiths03] and Web Services [Ballinger03, Ferrara02].
The Thread
class provides a number of methods and properties to work with a specific thread. There are certain members in the Thread
class, however, that you should not use, or use only with extreme caution. Here are some you should think twice about:
The IsAlive
property tells you if the thread represented by the Thread
instance is alive or not. The problem is that the thread may die right after you call this method, but before you make a decision based on that information.
The ThreadState
property has the same drawbacks as IsAlive
.
Now let’s look at some potentially dangerous methods:
The Suspend()
method pauses, freezes, or suspends a thread of execution. But it is a blunt instrument. Any resources locked by the thread are held while it is blocked. This may easily result in a deadlock. Suspend()
is not intended for normal day-to-day application programming. Do not use it, for example, to synchronize thread execution.
The Resume()
method reactivates a suspended thread. For the same reasons mentioned above in connection with Suspend()
, you should avoid it.
The ResetAbort()
method cancels an Abort()
request on a thread. This may surprise a programmer who has called Abort()
and has a reasonable expectation that the thread will go away.
If you think you need any of the methods mentioned above, you should revisit your design (For good discussions of multithreading issues, refer to [Lea00].) The most effective way to communicate between threads is to use what I call bait. Set some fields to a certain (boolean
) value and let the other thread check for that value to take a particular action or terminate. You might also consider other facilities, such as Wait()
, Pulse()
, Join()
, and synchronizing on a wait handle.
Certain methods and properties of the Thread
class have unpredictable behavior. Avoid them, or use them with extreme caution.
Gotcha #49, "Foreground threads may prevent a program from terminating,” Gotcha #53, "Environment.Exit() brings down the CLR,” and Gotcha #54, "ResetAbort() may lead to surprises.”
The IsBackground
property on a Thread
indicates whether the thread is a background (daemon) thread. The CLR quits executing if only background threads are running. In other words, in normal execution the CLR does not terminate as long as there is at least one non-background thread running.
In your application, if you start a thread and leave it running in the foreground (which is the default), then exit the program (by leaving Main()
or by clicking on the close box in a Windows application), the program continues to run as long as that thread runs. But if you set the thread to background, the program will terminate when you expect it to. Consider Example 7-1.
Example 7-1. Thread behavior
//Test.cs using System; using System.Threading; namespace Background { class Test { private static void Worker() { // Some activity Thread.Sleep(5000); Console.WriteLine("worker done at {0}", DateTime.Now.ToLongTimeString()); } [STAThread] static void Main(string[] args) { Thread workerThread = new Thread(new ThreadStart(Worker)); //workerThread.IsBackground = true; workerThread.Start(); Console.WriteLine("Main done at {0}", DateTime.Now.ToLongTimeString()); } } }
✗ VB.NET (IsBackground)
Imports System.Threading Module Test Private Sub Worker() 'Some activity Thread.Sleep(5000) Console.WriteLine("worker done at {0}", _ DateTime.Now.ToLongTimeString()) End Sub Sub Main() Dim workerThread As New Thread(AddressOf Worker) 'workerThread.IsBackground = true workerThread.Start() Console.WriteLine("Main done at {0}", _ DateTime.Now.ToLongTimeString()) End Sub End Module
In this example you create a Thread
instance, assign it to execute the Worker()
method, and start it. You print the message that Main()
is done and leave the Main()
method. When executed, the program will continue to run even after Main()
completes. It terminates only after the Worker()
method in the second thread completes, as shown in Figure 7-1.
If you uncomment the statement workerThread.IsBackground = true
, the program terminates when it leaves the Main()
method, as shown in Figure 7-2.
By default, threads are created in the foreground. Should you set IsBackground
to true
or not? Say you are performing a search in another thread. If the user quits the application, you may want the program to terminate. In this case, you need to set the thread to a background thread. On the other hand, if the task the other thread is performing is critical, and should be carried out no matter what other threads are running, then you don’t want to set it as background.
Before starting a Thread
, ask yourself if you should be setting the IsBackground
property to true
.
Gotcha #50, "Background threads don’t terminate gracefully,” Gotcha #53, "Environment.Exit() brings down the CLR,” and Gotcha #58, "Threads from the thread pool are scarce.”
In Gotcha #49, "Foreground threads may prevent a program from terminating ,” you saw the advantage of setting a thread as background. Such a thread runs only as long as a foreground thread is running. But if your background thread uses critical resources, you want to make sure that it terminates gracefully.
The MSDN documentation says,
Once all foreground threads belonging to a process have terminated, the common language runtime ends the process by invoking Abort on any background threads that are still alive.
If you write a method that executes in the background, you should expect its thread to terminate when all foreground threads quit. Unfortunately, contrary to what Microsoft’s documentation might lead you to expect, the termination is not graceful. Let’s look at Example 7-2.
Example 7-2. Abrupt termination of a background thread
using System; using System.Threading; namespace BackgroundThreadAndAbort { class Test { private static void Worker() { Console.WriteLine( "Worker started... given chance to cleanup?"); try { Thread.Sleep(5000); } catch(ThreadAbortException) { Console.WriteLine( "Thread aborted exception received"); } } [STAThread] static void Main(string[] args) { Thread workerThread = new Thread(new ThreadStart(Worker)); workerThread.IsBackground = true; workerThread.Start(); Thread.Sleep(2000); Console.WriteLine("Main done"); } } }
Imports System.Threading Module Test Private Sub Worker() Console.WriteLine( _ "Worker started... given chance to cleanup?") Try Thread.Sleep(5000) Catch ex As ThreadAbortException Console.WriteLine("Thread aborted exception received") End Try End Sub Public Sub Main() Dim workerThread As New Thread(AddressOf Worker) workerThread.IsBackground = True workerThread.Start() Thread.Sleep(2000) Console.WriteLine("Main done") End Sub End Module
In this program, you start a background thread that executes the Worker()
method. In that method you anticipate the ThreadAbortException
. When Main()
terminates, the background thread executing the Worker()
method is terminated. However, the ThreadAbortException
is not thrown on it. This is shown in the output in Figure 7-3. As a result, the background thread has no chance to clean up and exit gracefully. It just gets yanked.
Remember that background threads are killed abruptly when the process terminates and do not have an opportunity to clean up gracefully.
Gotcha #49, "Foreground threads may prevent a program from terminating" and Gotcha #53, "Environment.Exit() brings down the CLR.”
When you invoke the Interrupt()
method on a thread, the CLR throws a ThreadInterruptedException
in the thread’s context. However, this only happens when the thread is blocked—for instance, when it enters Sleep()
, Join()
, Wait()
, or requests a lock. If the thread is busy executing, the exception is not received until the thread blocks. So if you call Interrupt()
on a thread, the thread may not be interrupted for quite some time.
Furthermore, there is no guarantee that the thread will be interrupted at all. The thread may catch the ThreadInterruptedException
and just ignore it. Consider Example 7-3.
Example 7-3. Interrupting a thread
✗ C# (Interrupt)
using System; using System.Threading; namespace Interrupting { class Test { public static void Worker() { try { Console.WriteLine("worker started at {0}", DateTime.Now.ToLongTimeString()); string str = null; for(int i = 0; i < 30000; i++) { str += i.ToString(); // Simulating some activity } Thread.Sleep(1000); } catch(ThreadInterruptedException) { Console.WriteLine("Thread interrupted at {0}", DateTime.Now.ToLongTimeString()); } Console.WriteLine( "Continuing after Exception is caught"); } [STAThread] static void Main(string[] args) { Thread workerThread = new Thread( new ThreadStart(Worker)); //workerThread.IsBackground = true; workerThread.Start(); Thread.Sleep(1000); Console.WriteLine("Interrupting worker at {0}", DateTime.Now.ToLongTimeString()); workerThread.Interrupt(); } } }
Imports System.Threading Module Test Public Sub Worker() Try Dim str As String = Nothing Console.WriteLine("worker started at {0}", _ DateTime.Now.ToLongTimeString()) For i As Integer = 0 To 30000 str += i.ToString() 'Simulating some activity Next Thread.Sleep(1000) Catch ex As ThreadInterruptedException Console.WriteLine("Thread interrupted at {0}", _ DateTime.Now.ToLongTimeString()) End Try Console.WriteLine( _ "Continuing after Exception is caught") End Sub Public Sub Main() Dim workerThread As New Thread(AddressOf Worker) 'workerThread.IsBackground = True workerThread.Start() Thread.Sleep(1000) Console.WriteLine("Interrupting worker at {0}", _ DateTime.Now.ToLongTimeString()) workerThread.Interrupt() End Sub End Module
The Main()
method creates a thread to execute the Worker()
method. Worker()
enters a busy computation cycle concatenating strings. Then Main()
interrupts the thread. However, the ThreadInterruptedException
does not take effect until the thread finishes its computation and arrives at Sleep()
. When you execute the program, you get the output shown in Figure 7-4.
As you can see, even though the thread is interrupted one second after it was started, it did not receive the ThreadInterruptedException
until fourteen seconds later.
Remember that a thread receives a ThreadInterruptedException
only when it blocks, and that it is free to ignore the request.
Gotcha #52, "ThreadAbortException—a hot potato" and Gotcha #54, "ResetAbort() may lead to surprises.”
When you call Thread.Abort()
to abort a thread, the CLR throws a ThreadAbortException
on it. This allows the thread to clean up its resources and terminate gracefully. Unlike the ThreadInterruptedException
, the thread receives the ThreadAbortException
instantaneously. This can be dangerous at times. If you are in the middle of a call, or doing some important processing, it does not wait for you to finish. Of course, you can handle the ThreadAbortException
and stabilize your code. The ThreadAbortException
, however, is a special kind of exception. When you leave the catch
block, the CLR automatically throws it again, thus terminating the thread. Let’s take a look at this in Example 7-4.
Example 7-4. Behavior of Thread.Abort()
✗ C# (Abort)
using System; using System.Threading; namespace ThreadAborting { class Test { private static void Worker() { try { try { Thread.Sleep(5000); } catch(ThreadAbortException) { Console.WriteLine( "ThreadAbortException caught"); } Console.WriteLine( "Let's leave the method now"); } finally { Console.WriteLine("In the finally block"); } } [STAThread] static void Main(string[] args) { Thread workerThread = new Thread( new ThreadStart(Worker)); workerThread.Start(); Thread.Sleep(1000); Console.WriteLine("Calling abort"); workerThread.Abort(); Thread.Sleep(1000); Console.WriteLine("Main done"); } } }
✗ VB.NET (Abort)
Imports System.Threading Module Test Private Sub Worker() Try Try Thread.Sleep(5000) Catch ex As ThreadAbortException Console.WriteLine("ThreadAbortException caught") End Try Console.WriteLine("Let's leave the method now") Finally Console.WriteLine("In the finally block") End Try End Sub Public Sub Main() Dim workerThread As New Thread(AddressOf Worker) workerThread.Start() Thread.Sleep(1000) Console.WriteLine("Calling abort") workerThread.Abort() Thread.Sleep(1000) Console.WriteLine("Main done") End Sub End Module
In this example, Main()
starts a thread to run the Worker()
method. Then it invokes Abort()
on that thread. The thread receives the ThreadAbortException
right away. The exception is caught in the catch
block. Note that there is no throw
within the catch
block; however, the exception is re-thrown automatically. As a result, instead of the "Let's leave the method now
" message, the "In the finally block
" message gets printed, as shown in the output in Figure 7-5.
The ThreadAbortException
is a hot potato. The CLR throws it automatically from the catch
block in order to terminate the thread (Want to catch it again? No problem, it’ll be re-thrown again). There is no guarantee that any statements in your method will be executed. However, all the finally
blocks in the call stack are visited.
Write code defensively with the expectation that the thread may be aborted. Understand the special nature of the ThreadAbortException
.
Gotcha #51, "Interrupt () kicks in only when a thread is blocked,” Gotcha #54, "ResetAbort() may lead to surprises,” and Gotcha #55, "Abort() takes time to clean up.”
How do you exit from a thread? You do that by returning from its associated method. There is an Environment.Exit()
method, but it is somewhat more drastic: if you call the Exit()
method you terminate the process, no matter which thread you call it from. None of the application’s other threads gets a chance to clean up gracefully. Consider the code in Example 7-5.
Example 7-5. Behavior of Exit()
using System; using System.Threading; namespace Exit { class Test { public static void Worker() { Console.WriteLine("worker thread started"); try { Thread.Sleep(5000); } catch(ThreadInterruptedException) { Console.WriteLine("worker interrupted"); } catch(ThreadAbortException) { Console.WriteLine("worker aborted"); } } [STAThread] static void Main(string[] args) { Thread workerThread1 = new Thread( new ThreadStart(Worker)); workerThread1.Start(); Thread.Sleep(1000); Console.WriteLine("Interrupting worker1 at {0}", DateTime.Now.ToLongTimeString()); workerThread1.Interrupt(); Thread workerThread2 = new Thread( new ThreadStart(Worker)); workerThread2.Start(); Thread.Sleep(1000); Console.WriteLine("Calling Exit"); Environment.Exit(0); } } }
Imports System.Threading Module Test Public Sub Worker() Console.WriteLine("worker thread started") Try Thread.Sleep(5000) Catch ex As ThreadInterruptedException Console.WriteLine("worker interrupted") Catch ex As ThreadAbortException Console.WriteLine("worker aborted") End Try End Sub Public Sub Main() Dim workerThread1 As New Thread(AddressOf Worker) workerThread1.Start() Thread.Sleep(1000) Console.WriteLine("Interrupting worker1 at {0}", _ DateTime.Now.ToLongTimeString()) workerThread1.Interrupt() Dim workerThread2 As New Thread(AddressOf Worker) workerThread2.Start() Thread.Sleep(1000) Console.WriteLine("Calling Exit") Environment.Exit(0) End Sub End Module
You first create a thread and interrupt it after a one-second delay. From the output shown in Figure 7-6, you can see that the thread does get interrupted. Then you create another thread and after a one-second delay you call Exit()
. Note that the program terminates abruptly without giving the thread a chance to respond. The CLR terminates the process no matter which thread calls Exit()
. This is undesirable. You might consider using Exit()
in your application under special conditions, such as after dealing with an unhandled fatal exception. However, you need to exercise extreme caution in using Exit()
.
Thoroughly understand the consequence of calling Exit()
on a thread—you are bringing down the entire process.
Gotcha #49, "Foreground threads may prevent a program from terminating,” Gotcha #50, "Background threads don’t terminate gracefully,” and Gotcha #58, "Threads from the thread pool are scarce.”
In Gotcha #52, "ThreadAbortException—a hot potato,” you saw the special nature of ThreadAbortException
. The Thread
class provides the interesting method ResetAbort()
to overrule this exception. Let’s start by reviewing the code in Example 7-6.
Example 7-6. ResetAbort() at work
using System; using System.Threading; namespace ResetAbort { class Test { private static void Worker() { try { Thread.Sleep(5000); } catch(ThreadAbortException) { Console.WriteLine( "ThreadAbortException caught"); Thread.ResetAbort(); } Console.WriteLine("Look where we are now!"); Thread.Sleep(10000); } [STAThread] static void Main(string[] args) { Thread workerThread = new Thread(new ThreadStart(Worker)); workerThread.Start(); Thread.Sleep(1000); Console.WriteLine("Calling abort"); workerThread.Abort(); Thread.Sleep(2000); Console.WriteLine("Main done"); } } }
Imports System.Threading Module Test Private Sub Worker() Try Thread.Sleep(5000) Catch ex As ThreadAbortException Console.WriteLine("ThreadAbortException caught") Thread.ResetAbort() End Try Console.WriteLine("Look where we are now!") Thread.Sleep(10000) End Sub Public Sub Main() Dim workerThread As New Thread(AddressOf Worker) workerThread.Start() Thread.Sleep(1000) Console.WriteLine("Calling abort") workerThread.Abort() Thread.Sleep(2000) Console.WriteLine("Main done") End Sub End Module
When the CLR throws the ThreadAbortException
, within the catch
block you call ResetAbort()
. This cancels the Abort()
and the method continues running, as you can see in Figure 7-7.
What’s the issue here? The problem with ResetAbort()
is that the code that calls Abort()
won’t know that the Thread
has cancelled it. This can lead to unexpected (and unpredictable) behavior. If you find yourself doing something like this, you might need to redesign. Look at what you are trying to achieve and evaluate other ways to accomplish it (such as synchronization objects).
ResetAbort()
cancels an Abort()
. This is counter to normal expectations. Avoid using it and find clearer ways to achieve your goal.
Gotcha #51, "Interrupt () kicks in only when a thread is blocked" and Gotcha #52, "ThreadAbortException—a hot potato.”
As you saw in Gotcha #52, "ThreadAbortException—a hot potato,” when you abort a Thread
, the CLR throws a ThreadAbortException
on it. The thread can do whatever cleanup it needs to by handling the exception. The CLR then executes all the finally
blocks in the thread’s call stack before actually terminating it. So there might be a delay between the time you call Abort()
and the time the thread quits. This is illustrated in Example 7-7.
Example 7-7. Delay during Abort
using System; using System.Threading; namespace AbortAndJoin { class Test { private static void Worker() { Console.WriteLine("Worker started"); try { Thread.Sleep(5000); } finally { Console.WriteLine("Worker enters finally {0}", DateTime.Now.ToLongTimeString()); Thread.Sleep(10000); // Simulates some cleanup activity Console.WriteLine("Cleanup done in Worker {0}", DateTime.Now.ToLongTimeString()); } } [STAThread] static void Main(string[] args) { Thread workerThread = new Thread(new ThreadStart(Worker)); workerThread.IsBackground = true; workerThread.Start(); Thread.Sleep(1000); Console.WriteLine("Aborting thread {0}", DateTime.Now.ToLongTimeString()); workerThread.Abort(); workerThread.Join(); Console.WriteLine("Thread has aborted {0}", DateTime.Now.ToLongTimeString()); } } }
Imports System.Threading Module Test Private Sub Worker() Console.WriteLine("Worker started") Try Thread.Sleep(5000) Finally Console.WriteLine("Worker enters finally {0}", _ DateTime.Now.ToLongTimeString()) Thread.Sleep(10000) ' Simulates some cleanup activity Console.WriteLine("Cleanup done in Worker {0}", _ DateTime.Now.ToLongTimeString()) End Try End Sub Public Sub Main() Dim workerThread As New Thread(AddressOf Worker) workerThread.IsBackground = True workerThread.Start() Thread.Sleep(1000) Console.WriteLine("Aborting thread {0}", _ DateTime.Now.ToLongTimeString()) workerThread.Abort() workerThread.Join() Console.WriteLine("Thread has aborted {0}", _ DateTime.Now.ToLongTimeString()) End Sub End Module
In this example, the Worker()
method’s finally
block introduces a delay to simulate some activity. Main()
first starts a thread to run the Worker()
method. After a delay, it invokes Abort()
on that thread. Then it calls Join()
to wait for cleanup and completion. The output is shown in Figure 7-8.
If your application requires you to Abort()
a thread and perform some operation after that thread has quit, you should call Join()
on the thread after calling Abort()
. This waits for the thread to clean up properly and exit gracefully before you continue processing.
You may want to call the Join()
method with a reasonable timeout to avoid any potential starvation or deadlock, as in:
workerThread.Join(2000); // Wait two seconds for // the thread to complete
Ask yourself if you should call Join()
after a call to Abort()
. That is, ask if you need to wait for the thread to actually quit.
Gotcha #52, "ThreadAbortException—a hot potato" and Gotcha #54, "ResetAbort() may lead to surprises.”
Say you have a static
/Shared
method and you want to make sure that only one thread at a time can perform that operation. Typically, you use lock
/SyncLock
on an object to define a critical section. However, this won’t help if you are trying to synchronize within a static
/Shared
method or you’re trying to synchronize access to a static
/Shared
field. Static
/Shared
members are, by definition, independent from any object of the type. Consider Example 7-8.
Example 7-8. Ineffective synchronization
//Bacteria.cs using System; using System.Threading; namespace SynchOnType { public class Bacteria { private static int bacteriaCount; private static void IncreaseCount() { Console.WriteLine( "IncreaseCount called by {0} at {1}", AppDomain.GetCurrentThreadId(), DateTime.Now.ToLongTimeString()); bacteriaCount++; Thread.Sleep(2000); // Used for illustration purpose } public Bacteria() { lock(this) { IncreaseCount(); } } } } //Test.cs using System; using System.Threading; namespace SynchOnType { class Test { private static void Worker() { Console.WriteLine("In thread {0}", AppDomain.GetCurrentThreadId()); Bacteria aBacteria = new Bacteria(); } [STAThread] static void Main(string[] args) { Thread thread1 = new Thread(new ThreadStart(Worker)); Thread thread2 = new Thread(new ThreadStart(Worker)); thread1.Start(); thread2.Start(); } } }
'Bacteria.vb Imports System.Threading Public Class Bacteria Private Shared bacteriaCount As Integer Private Shared Sub IncreaseCount() Console.WriteLine( _ "IncreaseCount called by {0} at {1}", _ AppDomain.GetCurrentThreadId(), _ DateTime.Now.ToLongTimeString()) bacteriaCount += 1 Thread.Sleep(2000) ' Used for illustration purpose End Sub Public Sub New() SyncLock Me IncreaseCount() End SyncLock End Sub End Class 'Test.vb Imports System.Threading Module Test Private Sub Worker() Console.WriteLine("In thread {0}", _ AppDomain.GetCurrentThreadId()) Dim aBacteria As New Bacteria End Sub Public Sub Main() Dim thread1 As New Thread(AddressOf Worker) Dim thread2 As New Thread(AddressOf Worker) thread1.Start() thread2.Start() End Sub End Module
In this example, two threads create an object of the Bacteria
class almost at the same time. Within the constructor of Bacteria
you call a static
/Shared
method that requires synchronization, and you lock the instance before invoking the method.
The synchronization in the constructor is fundamentally flawed. The two threads in Test
create one Bacteria
object each. Each of the threads locks a different instance of Bacteria
, and then calls the static method. When the program is executed, the output shown in Figure 7-9 is generated.
As you can see, the two threads execute the IncreaseCount()
method at the same time, showing that neither is waiting the Sleep()
time (2000 milliseconds) for the other as expected. The synchronization is ineffective.
For two threads to treat a code segment as a critical section, they need to synchronize on the same instance. What is common to both the instances of Bacteria
? It’s the Bacteria
class itself, isn’t it? So, why not synchronize on the Bacteria
class, i.e., the metadata? The code that does just that is shown in Example 7-9.
In the constructor of Bacteria
you synchronize on the metadata of Bacteria
. You obtain this object by calling the GetType()
method on the instance. The output after the above change is shown in Figure 7-10.
The two threads are executing the IncreaseCount()
method two seconds apart, indicating that the calls to the method are being synchronized. It works, doesn’t it? Are you done? Yep, let’s ship it.
Well, sorry, the euphoria is short-lived. Let’s write a new class and change the Test
class, as in Example 7-10.
Example 7-10. Problem with instance’s GetType()
//SpecializedBacteria.cs using System; namespace SynchOnType { public class SpecializedBacteria : Bacteria { } } //Test.cs using System; using System.Threading; namespace SynchOnType { class Test { private static void Worker1() { Console.WriteLine("In thread {0}", AppDomain.GetCurrentThreadId()); Bacteria aBacteria = new Bacteria(); } private static void Worker2() { Console.WriteLine("In thread {0}", AppDomain.GetCurrentThreadId()); SpecializedBacteria aBacteria = new SpecializedBacteria(); } [STAThread] static void Main(string[] args) { Thread thread1 = new Thread(new ThreadStart(Worker1)); Thread thread2 = new Thread(new ThreadStart(Worker2)); thread1.Start(); thread2.Start(); } } }
'SpecializedBacteria.vb Public Class SpecializedBacteria Inherits Bacteria End Class 'Test.vb Imports System.Threading Module Test Private Sub Worker1() Console.WriteLine("In thread {0}", _ AppDomain.GetCurrentThreadId()) Dim aBacteria As New Bacteria End Sub Private Sub Worker2() Console.WriteLine("In thread {0}", _ AppDomain.GetCurrentThreadId()) Dim aBacteria As New SpecializedBacteria End Sub Public Sub Main() Dim thread1 As New Thread(AddressOf Worker1) Dim thread2 As New Thread(AddressOf Worker2) thread1.Start() thread2.Start() End Sub End Module
In this code, thread1
calls Worker1()
, which creates an instance of Bacteria
. In the meantime, thread2
calls Worker2()
, which creates an instance of SpecializedBacteria
. Recall that the constructor of Bacteria
is called when you create an instance of SpecializedBacteria
. Within the constructor, when you call GetType()
, which Type
metadata is returned? Because the instance being created is SpecializedBacteria
, GetType()
returns the Type
metadata for SpecializedBacteria
, not that of Bacteria
. Once again, the two threads end up locking different instances and get into the IncreaseCount()
method at the same time, as shown in Figure 7-11.
In this situation, locking on the metadata returned by GetType()
has no effect. What can you do to solve this problem? You want to lock the Bacteria
class. So, why not do that explicitly? Consider Example 7-11.
Here you fetch the Bacteria
’s Type
metadata by using the typeof
operator in C# and the GetType()
operator in VB.NET. This call returns a consistent metadata object regardless of the instance type. Now, when you execute the code, you get the desired output, as shown in Figure 7-12.
In this specific example, if your only objective is to synchronize the incrementing of the field, use Interlocked.Increment()
.
Are you done? Is this a solution you can be comfortable with? Well, not fully, but go with it for now while you look at more issues in the next gotcha.
If your intent is to fetch the metadata for a specific class, use the typeof
/GetType
operator instead of the GetType()
instance method.
Gotcha #20, "Singleton isn’t guaranteed process-wide,” Gotcha #57, "Locking on globally visible objects is too sweeping,” Gotcha #62, "Accessing WinForm controls from arbitrary threads is dangerous,” and Gotcha #64, "Raising events lacks thread-safety.”
In Gotcha #56, "Calling Type.GetType() may not return what you expect,” I discussed options for synchronizing threads. While the last solution presented there seems to work for that specific example, there is still a problem. Locking the type metadata to define a critical section is much too sweeping. If all static
/Shared
methods synchronize on the type metadata, then all access to the class methods is one-at-a-time, even if it doesn’t need to be. This certainly provides thread safety, but sacrifices concurrency in ways that are neither necessary nor desirable in most cases. Consider Example 7-12.
Example 7-12. Consequence of locking the Type metadata
//Bacteria.cs using System; using System.Threading; namespace SynchOnType { public class Bacteria { private static int bacteriaCount; private static void IncreaseCount() { Console.WriteLine( "IncreaseCount called by {0} at {1}", AppDomain.GetCurrentThreadId(), DateTime.Now.ToLongTimeString()); bacteriaCount++; Thread.Sleep(2000); // Used for illustration purpose } public Bacteria() { lock(typeof(Bacteria)) { IncreaseCount(); } } } } //Test.cs using System; using System.Threading; namespace SynchOnType { class Test { private static void Worker() { Console.WriteLine("In thread {0}", AppDomain.GetCurrentThreadId()); Bacteria aBacteria = new Bacteria(); } [STAThread] static void Main(string[] args) { Thread thread1 = new Thread(new ThreadStart(Worker)); Thread thread2 = new Thread(new ThreadStart(Worker)); lock(typeof(Bacteria)) { Console.WriteLine("Starting threads at {0}", DateTime.Now.ToLongTimeString()); thread1.Start(); thread2.Start(); Thread.Sleep(3000); } } } }
Imports System.Threading 'Bacteria.vb Public Class Bacteria Private Shared bacteriaCount As Integer Private Shared Sub IncreaseCount() Console.WriteLine( _ "IncreaseCount called by {0} at {1}", _ AppDomain.GetCurrentThreadId(), _ DateTime.Now.ToLongTimeString()) bacteriaCount += 1 Thread.Sleep(2000) ' Used for illustration purpose End Sub Public Sub New() SyncLock GetType(Bacteria) IncreaseCount() End SyncLock End Sub End Class 'Test.vb Imports System.Threading Module Test Private Sub Worker() Console.WriteLine("In thread {0}", _ AppDomain.GetCurrentThreadId()) Dim aBacteria As New Bacteria End Sub Public Sub Main() Dim thread1 As New Thread(AddressOf Worker) Dim thread2 As New Thread(AddressOf Worker) SyncLock (GetType(Bacteria)) Console.WriteLine("Starting threads at {0}", _ DateTime.Now.ToLongTimeString()) thread1.Start() thread2.Start() Thread.Sleep(3000) End SyncLock End Sub End Module
When you execute the above program, since the Main()
method holds a lock on the Bacteria
type for 3 seconds, the creation of the Bacteria
objects is delayed until Main()
lets go of that lock. One reason why a client of the Bacteria
class might hold a lock on its metadata is so it can call multiple static
/Shared
methods in a thread-safe way. For whatever reason, if Main()
or any other method in the application grabs a lock on the Bacteria
metadata, the execution of the constructors (and all other methods) is delayed. This is shown in the output in Figure 7-13.
The problem here is that Bacteria
’s constructor claims a lock on the Bacteria
type in order to synchronize the execution of its static
/Shared
method. The static
/Shared
method and the implementation of the constructor are purely private to the class. However, you rely on a publicly visible object (the metadata of Bacteria
) to synchronize. You can lock much more locally and precisely. Let’s look at a modified implementation in Example 7-13.
Example 7-13. Synchronizing locally and precisely
//Bacteria.cs using System; namespace SynchOnType { public class Bacteria { private static int bacteriaCount; private static object theIncrementCountLock = new Object(); private static void IncreaseCount() { Console.WriteLine( "IncreaseCount called by {0} at {1}", AppDomain.GetCurrentThreadId(), DateTime.Now.ToLongTimeString()); bacteriaCount++; System.Threading.Thread.Sleep(2000); // Used for illustration purpose } public Bacteria() { lock(theIncrementCountLock) { IncreaseCount(); } } } }
✓ VB.NET (SynchWithIntent)
Imports System.Threading 'Bacteria.vb Public Class Bacteria Private Shared bacteriaCount As Integer Private Shared theIncrementCountLock As New Object Private Shared Sub IncreaseCount() Console.WriteLine( _ "IncreaseCount called by {0} at {1}", _ AppDomain.GetCurrentThreadId(), _ DateTime.Now.ToLongTimeString()) bacteriaCount += 1 Thread.Sleep(2000) ' Used for illustration purpose End Sub Public Sub New() SyncLock theIncrementCountLock IncreaseCount() End SyncLock End Sub End Class
In this case, you have created a dummy object (theIncrementCountLock
refers to it) within the Bacteria
class. In the constructor, you lock this object. Note that the dummy lock-facilitator object is private, so no method outside the class can claim a lock with it.
There are two advantages to this approach. One, code in other classes can’t affect the concurrency of the constructor, because they can’t see theIncrementCountLock
. Second, suppose you have two different tasks, call them Task A and Task B. Each of these tasks must be executed one-at-a-time. But if Task A doesn’t access any resources that Task B needs, and Task B touches none of Task A’s resources, there is no problem with their running concurrently. You can realize this most effectively using two private dummy-lock objects. The output after this change is shown in Figure 7-14.
For a great discussion on possible deadlock consequences, refer to the article "Don’t Lock Type Objects!" in the "on the web" section of the Appendix.
Do not synchronize within your class on publicly visible objects. Synchronize precisely by synchronizing locally.
Gotcha #20, "Singleton isn’t guaranteed process-wide,” Gotcha #56, "Calling Type.GetType() may not return what you expect,” Gotcha #62, "Accessing WinForm controls from arbitrary threads is dangerous,” and Gotcha #64, "Raising events lacks thread-safety.”
Should you create a thread or use a thread from the thread pool? There are several considerations here.
First, if you create a Thread
object, it will start to execute the method of interest within a fairly brief interval (subject to system limitations). In the case of the thread pool, however, you are using shared resources in your process. If another task that uses the thread pool is taking a little longer, your task may get delayed because there is no available thread in the thread pool.
At any given time up to twenty five threads are available per process per processor. You can change this default value at the system level. What does that mean to your application? If your tasks take a short amount of time and you have only a few of them, it is efficient to use the thread pool. However, if your tasks may take an arbitrary amount of time, then using the thread pool may not be a good idea.
Let’s get a better understanding of these issues from Example 7-14.
Example 7-14. Using your own thread versus one from thread pool
using System; using System.Threading; namespace ThreadPool { class Test { private static void Method1() { Console.Write("Executed by Thread {0} which is ", AppDomain.GetCurrentThreadId()); if (!Thread.CurrentThread.IsThreadPoolThread) Console.Write("not "); Console.WriteLine("from the thread pool at {0}", DateTime.Now.ToLongTimeString()); //Thread.Sleep(10000); } [STAThread] static void Main(string[] args) { Console.WriteLine("Using our own thread"); for(int i = 0; i < 5; i++) { new Thread(new ThreadStart(Method1)).Start(); Thread.Sleep(1000); } Console.WriteLine("Press any key to use timer"); Console.ReadLine(); Console.WriteLine("Using timer"); System.Timers.Timer theTimer = new System.Timers.Timer(1000); theTimer.Elapsed += new System.Timers.ElapsedEventHandler( theTimer_Elapsed); theTimer.Start(); Thread.Sleep(6000); theTimer.Stop(); } private static void theTimer_Elapsed( object sender, System.Timers.ElapsedEventArgs e) { Method1(); } } }
✗ VB.NET (ThreadFromPool)
Imports System.Threading Module Test Private Sub Method1() Console.Write("Executed by Thread {0} which is ", _ AppDomain.GetCurrentThreadId()) If Not Thread.CurrentThread.IsThreadPoolThread Then Console.Write("not ") End If Console.WriteLine("from the thread pool at {0}", _ DateTime.Now.ToLongTimeString()) 'Thread.Sleep(10000) End Sub Public Sub Main() Console.WriteLine("Using our own thread") For i As Integer = 0 To 4 Dim aThread As New Thread(AddressOf Method1) aThread.Start() Thread.Sleep(1000) Next Console.WriteLine("Press any key to use timer") Console.ReadLine() Console.WriteLine("Using timer") Dim theTimer As New System.Timers.Timer(1000) AddHandler theTimer.Elapsed, _ New System.Timers.ElapsedEventHandler( _ AddressOf theTimer_Elapsed) theTimer.Start() Thread.Sleep(6000) theTimer.Stop() End Sub Private Sub theTimer_Elapsed( _ ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs) Method1() End Sub End Module
In the previous example, you first run Method1()
using threads you create. Then you run the method using a System.Timers.Timer
object, which uses a thread from the thread pool. The output from the program is shown in Figure 7-15.
Notice that all the calls to Method1()
using the timer were executed by the same thread from the thread pool. Now, what happens if you put a delay in the method? Executing the code after uncommenting Thread.Sleep(10000)
, you get the output shown in Figure 7-16.
Different threads from the thread pool now execute the method. However, the calls to the method using the thread pool are not distributed evenly once per second (note that two calls to Method1()
were made at 8:19:48 PM).
Furthermore, let’s see what happens if you change Thread.Sleep(10000)
to Thread.Sleep(35000)
and let the Timer
run for more than 35 seconds. Partial output from this change is shown in Example 7-15.
Example 7-15. Partial output upon increasing the delay in Method1()
Using timer Executed by Thread 2864 which is from the thread pool at 8:21:55 PM Executed by Thread 2196 which is from the thread pool at 8:21:57 PM Executed by Thread 4064 which is from the thread pool at 8:21:57 PM Executed by Thread 1536 which is from the thread pool at 8:21:58 PM Executed by Thread 2492 which is from the thread pool at 8:21:59 PM Executed by Thread 3024 which is from the thread pool at 8:22:00 PM Executed by Thread 2588 which is from the thread pool at 8:22:01 PM Executed by Thread 3000 which is from the thread pool at 8:22:02 PM Executed by Thread 2932 which is from the thread pool at 8:22:03 PM Executed by Thread 3952 which is from the thread pool at 8:22:04 PM Executed by Thread 3004 which is from the thread pool at 8:22:05 PM ... Executed by Thread 2524 which is from the thread pool at 8:22:11 PM Executed by Thread 3016 which is from the thread pool at 8:22:12 PM Executed by Thread 3012 which is from the thread pool at 8:22:13 PM Executed by Thread 3160 which is from the thread pool at 8:22:14 PM Executed by Thread 3156 which is from the thread pool at 8:22:15 PM Executed by Thread 2992 which is from the thread pool at 8:22:16 PM Executed by Thread 3008 which is from the thread pool at 8:22:17 PM Executed by Thread 2984 which is from the thread pool at 8:22:18 PM Executed by Thread 3040 which is from the thread pool at 8:22:19 PM Executed by Thread 2864 which is from the thread pool at 8:22:30 PM Executed by Thread 2196 which is from the thread pool at 8:22:32 PM Executed by Thread 4064 which is from the thread pool at 8:22:32 PM Executed by Thread 1536 which is from the thread pool at 8:22:33 PM Executed by Thread 2492 which is from the thread pool at 8:22:34 PM
The threads in the thread pool are reused once they become available. This is of course good news. However, notice the lag time between the method executions when all the threads in the thread pool are busy.
The thread pool offers a very efficient use of resources, but it should only be used for tasks that are quick and short-lived.
Gotcha #50, "Background threads don’t terminate gracefully,” Gotcha #53, "Environment.Exit() brings down the CLR,” Gotcha #59, "Threads invoked using delegates behave like background threads,” Gotcha #61, "Exceptions thrown from threads in the pool are lost,” and Gotcha #62, "Accessing WinForm controls from arbitrary threads is dangerous.”
If you want a method to be executed from another thread, you can create a thread, set it to background, and then start it. That is three lines of code at least, isn’t it? What if your method takes parameters? That is even more work. This is where the Delegate
class’s BeginInvoke()
and EndInvoke()
methods look very enticing. Delegates provide BeginInvoke()
and EndInvoke()
to call a method asynchronously. The intended method is invoked from another thread, and the code is pretty easy to write.
Let’s see how you can do just that in Example 7-16.
Example 7-16. Using Delegate to call methods in another thread
using System; namespace DelegateThread { class Test { private static void Method1(int val) { Console.Write("Method 1 called from Thread {0}", AppDomain.GetCurrentThreadId()); Console.WriteLine(" with value {0}", val); } delegate void Method1Delegate(int val); [STAThread] static void Main(string[] args) { // It is not so easy to call Method1 from // another thread using the Thread class. Method1Delegate dlg = new Method1Delegate(Method1); dlg.BeginInvoke(2, null, null); Console.ReadLine(); } } }
✗ VB.NET (BeginInvoke)
Module Test Private Sub Method1(ByVal val As Integer) Console.Write("Method 1 called from Thread {0}", _ AppDomain.GetCurrentThreadId()) Console.WriteLine(" with value {0}", val) End Sub Delegate Sub Method1Delegate(ByVal val As Integer) Public Sub Main() ' It is not so easy to call Method1 from ' another thread using the Thread class. Dim dlg As New Method1Delegate(AddressOf Method1) dlg.BeginInvoke(2, Nothing, Nothing) Console.ReadLine() End Sub End Module
In this example, you simply create an object of the delegate Method1Delegate
and call its BeginInvoke()
method, passing the desired value for its first argument. The two null
/Nothing
arguments indicate that you are not passing a callback method or AsyncState object. The output is shown in Figure 7-17.
If you are able to execute the method in another thread so easily, why would you do it any other way? Well, this is perfectly OK, but you need to understand the benefits and drawbacks:
The method is executed by a thread from the thread pool.
The method is executed by a thread that is marked as a background thread.
Exceptions thrown from the method are lost unless you call EndInvoke()
, even for methods that do not return any results.
What do these considerations imply? Since you use a thread from the thread pool, its execution depends on the availability of a thread (see “Gotcha #58, "Threads from the thread pool are scarce). Since it is a background thread, it will be terminated if all foreground threads terminate (see Gotcha #49, "Foreground threads may prevent a program from terminating" and Gotcha #50, "Background threads don’t terminate gracefully“). Also, since it is from the thread pool, any exceptions thrown in it are handled by the pool. As a result you may not even realize that an exception occurred until you call EndInvoke()
(see Gotcha #61, "Exceptions thrown from threads in the pool are lost“). With a C# void
method, or a VB.NET Sub
, you normally have no reason to call EndInvoke()
. But you have to if you want to know if the method completed successfully. Finally, you don’t want to synchronize with any other threads while running in the context of a thread-pool thread. If you do, it may delay the start of other tasks that need a thread from the thread pool.
Fully understand the behavior and limitations of using Delegate.BeginInvoke()
. It has all the advantages and limitations of using a background thread and a thread from the thread pool.
Gotcha #49, "Foreground threads may prevent a program from terminating,” Gotcha #50, "Background threads don’t terminate gracefully,” Gotcha #58, "Threads from the thread pool are scarce,” and Gotcha #61, "Exceptions thrown from threads in the pool are lost.”
Say you want to call a method of your class in a separate thread of execution. To start a new thread, you create a Thread
object, provide it with a ThreadStart
delegate, and call the Start()
method on the Thread
instance. The ThreadStart
delegate, however, only accepts methods that take no parameters. So how can you pass parameters to the thread?
One option is to use a different Delegate
(as discussed in Gotcha #59, "Threads invoked using delegates behave like background threads“) and call the method asynchronously using the Delegate.BeginInvoke()
method. This is by far the most convenient option. However, this executes the method in a thread from the thread pool. Therefore, its time of execution has to stay pretty short. Otherwise you end up holding resources from the thread pool and may slow down the start of other tasks.
Let’s consider Example 7-17, in which you want to start a thread and pass it an integer parameter.
Example 7-17. Calling method with parameter from another thread
//SomeClass.cs part of ALib.dll using System; using System.Threading; namespace ALib { public class SomeClass { private void Method1(int val) { // Some operation takes place here Console.WriteLine( "Method1 runs on Thread {0} with {1}", AppDomain.GetCurrentThreadId(), val); } public void DoSomething(int val) { // Some operation... // Want to call Method1 in different thread // from here? // Some operation... } } } //Test.cs part of TestApp.exe using System; using ALib; namespace TestApp { class Test { [STAThread] static void Main(string[] args) { Console.WriteLine("Main running in Thread {0}", AppDomain.GetCurrentThreadId()); SomeClass anObject = new SomeClass(); anObject.DoSomething(5); } } }
'SomeClass.vb part of ALib.dll Imports System.Threading Public Class SomeClass Private Sub Method1(ByVal val As Integer) Console.WriteLine( _ "Method1 runs on Thread {0} with {1}", _ AppDomain.GetCurrentThreadId(), val) ' Some operation takes place here End Sub Public Sub DoSomething(ByVal val As Integer) ' Some operation... ' Want to call Method1 in different thread ' from here? ' Some operation... End Sub End Class 'Test.vb Imports ALib Module Test Public Sub Main() Console.WriteLine("Main running in Thread {0}", _ AppDomain.GetCurrentThreadId()) Dim anObject As New SomeClass anObject.DoSomething(5) End Sub End Module
In this example, SomeClass
.DoSomething()
wants to call Method1()
in a different thread. Unfortunately, you can’t just create a new Thread
instance and pass a ThreadStart
delegate with the address of Method1()
. How can you invoke Method1()
from here? One approach is shown in Example 7-18.
Example 7-18. One approach to invoking method with parameter
//SomeClass.cs part of ALib.dll using System; using System.Threading; namespace ALib { public class SomeClass { private void Method1(int val) { // Some operation takes place here Console.WriteLine( "Method1 runs on Thread {0} with {1}", AppDomain.GetCurrentThreadId(), val); } private int theValToUseByCallMethod1; private void CallMethod1() { Method1(theValToUseByCallMethod1); } public void DoSomething(int val) { // Some operation... // Want to call Method1 in different thread // from here? theValToUseByCallMethod1 = val; new Thread(new ThreadStart(CallMethod1)).Start(); // Some operation... } } }
'SomeClass.vb part of ALib.dll Imports System.Threading Public Class SomeClass Private Sub Method1(ByVal val As Integer) Console.WriteLine( _ "Method1 runs on Thread {0} with {1}", _ AppDomain.GetCurrentThreadId(), val) ' Some operation takes place here End Sub Private theValToUseByCallMethod1 As Integer Private Sub CallMethod1() Method1(theValToUseByCallMethod1) End Sub Public Sub DoSomething(ByVal val As Integer) ' Some operation... ' Want to call Method1 in different thread ' from here? theValToUseByCallMethod1 = val Dim aThread As New Thread(AddressOf CallMethod1) aThread.Start() ' Some operation... End Sub End Class
In the DoSomething()
method, you first store the argument you want to pass to the thread in the private
field theValToUseByCallMethod1
. Then you call a no-parameter method CallMethod1()
in a different thread. CallMethod1()
, executing in this new thread, picks up the private
field set by the main thread and calls Method1()
with it. The output from the above code is shown in Figure 7-18.
See, it works! Well, yes, if a click of a button is going to call DoSomething()
, then the chances of DoSomething()
being called more than once before Method1()
has had a chance to execute are slim. But if this is invoked from a class library, or from multiple threads in the UI itself, how thread-safe is the code? Not very.
Let’s add a line to the Main()
method as shown in Example 7-19.
Example 7-19. Testing thread safety of approach in Example 7-18
static void Main(string[] args)
{
Console.WriteLine("Main running in Thread {0}",
AppDomain.GetCurrentThreadId());
SomeClass anObject = new SomeClass();
anObject.DoSomething(5);
anObject.DoSomething(6);
}
✗ VB.NET (ParamThreadSafety)
Public Sub Main()
Console.WriteLine("Main running in Thread {0}", _
AppDomain.GetCurrentThreadId())
Dim anObject As New SomeClass
anObject.DoSomething(5)
anObject.DoSomething(6)
End Sub
Here, you invoke the DoSomething()
method with a value of 6
immediately after calling it with a value of 5
. Let’s look at the output from the program after this change, shown in Figure 7-19.
As you can see, both calls to DoSomething()
pass Method1()
the value 6
. The value of 5
you send in the first invocation is simply overwritten.
One way to attain thread safety in this situation is to isolate the value in a different object. This is shown in Example 7-20.
Example 7-20. Providing thread safety of parameter
//... class CallMethod1Helper { private SomeClass theTarget; private int theValue; public CallMethod1Helper(int val, SomeClass target) { theValue = val; theTarget = target; } private void CallMethod1() { theTarget.Method1(theValue); } public void Run() { new Thread( new ThreadStart(CallMethod1)).Start(); } } public void DoSomething(int val) { // Some operation... // Want to call Method1 in different thread // from here? CallMethod1Helper helper = new CallMethod1Helper( val, this); helper.Run(); // Some operation... }
'... Class CallMethod1Helper Private theTarget As SomeClass Private theValue As Integer Public Sub New(ByVal val As Integer, ByVal target As SomeClass) theValue = val theTarget = target End Sub Private Sub CallMethod1() theTarget.Method1(theValue) End Sub Public Sub Run() Dim theThread As New Thread(AddressOf CallMethod1) theThread.Start() End Sub End Class Public Sub DoSomething(ByVal val As Integer) ' Some operation... ' Want to call Method1 in different thread ' from here? Dim helper As New CallMethod1Helper(val, Me) helper.Run() ' Some operation... End Sub
In this case, you create a nested helper class ClassMethod1Helper
that holds the val
and a reference to the object of SomeClass
. You invoke the Run()
method on an instance of the helper in the original thread. Run()
in turn invokes CallMethod1()
of the helper in a separate thread. This method calls Method1()
. Since the instance of helper is created within the DoSomething()
method, multiple calls to DoSomething()
will result in multiple helper objects being created on the heap. They are isolated from one another and provide thread safety for the parameter. The output from the program is shown in Figure 7-20.
In this example, it took over 20 lines of code (with proper indentation, that is) to create a thread-safe start of Method1()
. If you need to invoke another method with parameters, you will have to write almost the same amount of code. You will end up writing a class for each method you want to call. This is quite a bit of redundant coding.
Why not write your own thread-safe thread starter? The code to start the thread might look like Example 7-21 (using the ThreadRunner
class, which you’ll see shortly).
Example 7-21. Using ThreadRunner
//SomeClass.cs part of ALib.dll using System; using System.Threading; namespace ALib { public class SomeClass { private void Method1(int val) { // Some operation takes place here Console.WriteLine( "Method1 runs on Thread {0} with {1}", AppDomain.GetCurrentThreadId(), val); } private delegate void CallMethod1Delegate(int val); public void DoSomething(int val) { // Some operation... // Want to call Method1 in different thread // from here? ThreadRunner theRunner = new ThreadRunner( new CallMethod1Delegate(Method1), val); theRunner.Start(); // Some operation... } } }
'SomeClass.vb part of ALib.dll Imports System.Threading Public Class SomeClass Private Sub Method1(ByVal val As Integer) Console.WriteLine( _ "Method1 runs on Thread {0} with {1}", _ AppDomain.GetCurrentThreadId(), val) ' Some operation takes place here End Sub Private Delegate Sub CallMethod1Delegate(ByVal val As Integer) Public Sub DoSomething(ByVal val As Integer) ' Some operation... ' Want to call Method1 in different thread ' from here? Dim theRunner As New ThreadRunner( _ New CallMethod1Delegate(AddressOf Method1), val) theRunner.Start() ' Some operation... End Sub End Class
That is sweet and simple. You create a ThreadRunner
object and send it a delegate with the same signature as the method you’re going to call. You also send it the parameters you want to pass. The ThreadRunner
launches a new thread to execute the method that is referred to by the given delegate.
The code for ThreadRunner
is shown in Example 7-22. Note that you do not write a ThreadRunner
for each method you want to call. Unlike the CallMethod1Helper
, this is a class written once and used over and over.
Example 7-22. ThreadRunner class
//ThreadRunner.cs using System; using System.Threading; namespace ALib { public class ThreadRunner { private Delegate toRunDelegate; private object[] toRunParameters; private Thread theThread; public bool IsBackground { get { return theThread.IsBackground; } set { theThread.IsBackground = value; } } public ThreadRunner(Delegate theDelegate, params object[] theParameters) { toRunDelegate = theDelegate; toRunParameters = theParameters; theThread = new Thread(new ThreadStart(Run)); } public void Start() { theThread.Start(); } private void Run() { toRunDelegate.DynamicInvoke(toRunParameters); } } }
'ThreadRunner.vb Imports System.Threading Public Class ThreadRunner Private toRunDelegate As System.Delegate Private toRunParameters() As Object Private theThread As Thread Public Property IsBackground() As Boolean Get Return theThread.IsBackground End Get Set(ByVal Value As Boolean) theThread.IsBackground = Value End Set End Property Public Sub New(ByVal theDelegate As System.Delegate, _ ByVal ParamArray theParameters() As Object) toRunDelegate = theDelegate toRunParameters = theParameters theThread = New Thread(AddressOf Run) End Sub Public Sub Start() theThread.Start() End Sub Private Sub Run() toRunDelegate.DynamicInvoke(toRunParameters) End Sub End Class
Because ThreadRunner
exposes the IsBackground
property of its underlying Thread
object, a user of ThreadRunner
can set IsBackground
to true
if desired. To start the target method in a separate thread, the user of this class calls ThreadRunner.Start()
.
How does this differ in .NET 2.0 Beta 1? A new delegate named ParameterizedThreadStart
is introduced. Using this new delegate, you may invoke methods that take one parameter by passing the argument to the Thread
class’s Start()
method.
When you start a thread with a method that requires parameters, wrapper classes like ThreadRunner
(see Example 7-22) can help ensure thread safety.
If any exceptions are thrown from a thread you create and you don’t handle them, the CLR reports them to the user. In a console application, a message is printed on the console. In a Windows application, a dialog appears with the details of the exception. Of course, a program that displays such unhandled exceptions is undesirable. However, you might also agree that such a program is better than a program that continues to execute and quietly misbehaves after things go wrong. Let’s look at Example 7-23.
Example 7-23. Unhandled exception
using System; using System.Threading; namespace DelegateThread { class Test { private static void Method1() { Console.WriteLine("Method1 is throwing exception"); throw new ApplicationException("**** oops ****"); } delegate void Method1Delegate(); [STAThread] static void Main(string[] args) { Console.WriteLine("We first use a thread"); Thread aThread = new Thread(new ThreadStart(Method1)); aThread.Start(); Console.WriteLine("press return"); Console.ReadLine(); Console.WriteLine("We will use a Delegate now"); Method1Delegate dlg = new Method1Delegate(Method1); IAsyncResult handle = dlg.BeginInvoke(null, null); Thread.Sleep(1000); Console.WriteLine("Was the exception reported so far?"); try { Console.WriteLine("Let's call EndInvoke"); dlg.EndInvoke(handle); } catch(Exception ex) { Console.WriteLine("Exception: {0}", ex.Message); } Console.WriteLine("press return"); Console.ReadLine(); Console.WriteLine("We will use a timer now"); System.Timers.Timer theTimer = new System.Timers.Timer(1000); theTimer.Elapsed += new System.Timers.ElapsedEventHandler( theTimer_Elapsed); theTimer.Start(); Thread.Sleep(3000); theTimer.Stop(); Console.WriteLine("press return"); Console.ReadLine(); } private static void theTimer_Elapsed( object sender, System.Timers.ElapsedEventArgs e) { Method1(); } } }
✗ VB.NET (ExceptionInThread)
Imports System.Threading
Module Test
Private Sub Method1()
Console.WriteLine("Method1 is throwing exception")
Throw New ApplicationException("**** oops ****")
End Sub
Delegate Sub Method1Delegate()
Public Sub Main()
Console.WriteLine("We first use a thread")
Dim aThread As New Thread(AddressOf Method1)
aThread.Start()
Console.WriteLine("press return")
Console.ReadLine()
Console.WriteLine("We will use a Delegate now")
Dim dlg As New Method1Delegate(AddressOf Method1)
Dim handle As IAsyncResult = dlg.BeginInvoke(Nothing, Nothing)
Thread.Sleep(1000)
Console.WriteLine("Was the exception reported so far?")
Try
Console.WriteLine("Let's call EndInvoke")
dlg.EndInvoke(handle)
Catch ex As Exception
Console.WriteLine("Exception: {0}", ex.Message)
End Try
Console.WriteLine("press return")
Console.ReadLine()
Console.WriteLine("We will use a timer now")
Dim theTimer As New System.Timers.Timer(1000)
AddHandler theTimer.Elapsed, _
New System.Timers.ElapsedEventHandler( _
AddressOf theTimer_Elapsed)
theTimer.Start()
Thread.Sleep(3000)
theTimer.Stop()
Console.WriteLine("press return")
Console.ReadLine()
End Sub
Private Sub theTimer_Elapsed( _
ByVal sender As Object, ByVal e As System.Timers.ElapsedEventArgs)
Method1()
End Sub
End Module
VB.NET (ExceptionInThread)
In this example, you have a method Method1()
that throws an exception. You first call this method from our own thread. The CLR reports this exception by displaying a message on the console. Then you invoke the method using Method1Delegate
.BeginInvoke()
. The exception is not caught or reported when it is thrown. It does surface eventually when you call the delegate’s EndInvoke()
. The worst offender is the Timer
, where the exception simply goes unnoticed. The output from the above program is shown in Figure 7-21.
Why is this important to know? Consider invoking a web service asynchronously. You set up a delegate to it and in the callback, you call EndInvoke()
. The problem is that the callback itself is called from a thread in the thread pool. If an exception is raised in the callback, it is never seen. This is illustrated in Examples 7-24 through 7-27, and in Figures 7-22 and 7-23.
Example 7-24. Lost exception in asynchronous call, Web service (C#)
✗ C# (ExceptionInThread), server-side code
//MyService.asmx.cs part of ACSWebService.dll (Web Service) using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Web; using System.Web.Services; namespace ACSWebService { [WebService(Namespace="MyServiceNameSpace")] public class MyService : System.Web.Services.WebService { // ... [WebMethod] public int Method1(int val) { if (val == 0) throw new ApplicationException( "Do not like the input"); return val; } } }
Example 7-25. Lost exception in asynchronous call, Web service client (C#)
✗ C# (ExceptionInThread), client-side code
///Form1.cs part of AWSClient using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.Threading; using AWSClient.ACSWebService; namespace AWSClient { public class Form1 : System.Windows.Forms.Form { private static MyService service; // Parts of this file not shown... private void InvokeButton_Click( object sender, System.EventArgs e) { service = new MyService(); service.Credentials = System.Net.CredentialCache.DefaultCredentials; if (AsynchCheckBox.Checked) { CallAsynch(); } else { CallSynch(); } } private static void CallSynch() { int result = service.Method1(1); MessageBox.Show("Result received: " + result); result = service.Method1(0); MessageBox.Show("Result received: " + result); } private static void CallAsynch() { service.BeginMethod1(1, new AsyncCallback(response), service); Thread.Sleep(1000); service.BeginMethod1(0, new AsyncCallback(response), service); Thread.Sleep(1000); } private static void response(IAsyncResult handle) { MyService theService = handle.AsyncState as MyService; int result = theService.EndMethod1(handle); MessageBox.Show("Result received asynchronously " + result); } } }
Example 7-26. Lost exception in asynchronous call, Web service (VB.NET)
✗ VB.NET (ExceptionInThread), server-side code
'MyService.asmx.vb part of AVBWebService.dll (Web Service)
Imports System.Web.Services
<System.Web.Services.WebService(Namespace:="MyServiceNameSpace")> _
Public Class MyService
Inherits System.Web.Services.WebService
'...
<WebMethod()> _
Public Function Method1(ByVal val As Integer) As Integer
If val = 0 Then
Throw New ApplicationException("Do not like the input")
End If
Return val
End Function
End Class
Example 7-27. Lost exception in asynchronous call, Web service client (VB.NET)
✗ VB.NET (ExceptionInThread), client-side code
'Form1.vb part of AWSClient Imports System.Threading Imports AWSClient.AVBWebService Public Class Form1 Inherits System.Windows.Forms.Form Private Shared service As MyService 'Parts of this file not shown... Private Sub InvokeButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles InvokeButton.Click service = New MyService service.Credentials = _ System.Net.CredentialCache.DefaultCredentials If AsynchCheckBox.Checked Then CallAsynch() Else CallSynch() End If End Sub Private Shared Sub CallSynch() Dim result As Integer = service.Method1(1) MessageBox.Show("Result received: " & result) result = service.Method1(0) MessageBox.Show("Result received: " & result) End Sub Private Shared Sub CallAsynch() service.BeginMethod1(1, _ New AsyncCallback(AddressOf response), service) Thread.Sleep(1000) service.BeginMethod1(0, _ New AsyncCallback(AddressOf response), service) Thread.Sleep(1000) End Sub Private Shared Sub response(ByVal handle As IAsyncResult) Dim theService As MyService = _ CType(handle.AsyncState, MyService) Dim result As Integer = theService.EndMethod1(handle) MessageBox.Show("Result received asynchronously " & _ result) End Sub End Class
The web service throws an exception if the parameter to Method1()
is zero. From a Windows application, you invoke the web service, first sending a value of 1 and then a value of 0. When you invoke the method synchronously with an argument of zero, the CLR displays an exception, as shown in Figure 7-24.
However, if you call it asynchronously, no exception is reported. This is because the callback itself executes on a thread pool thread. You might expect the exception to be received when you call EndInvoke()
in the response()
method. Yes, an exception is raised (a System.Web.Services.Protocols.SoapException
, to be precise). But it isn’t propagated to you; the CLR suppresses any exception raised from a thread in the thread pool. You can verify this by adding a try
/catch
block around the EndInvoke()
call, as shown in Example 7-28.
Example 7-28. Catching exception after call to EndInvoke()
✓ C# (ExceptionInThread), client-side code
private static void response(IAsyncResult handle) { MyService theService = handle.AsyncState as MyService; try { int result = theService.EndMethod1(handle); MessageBox.Show("Result received asynchronously " + result); } catch(System.Web.Services.Protocols.SoapException ex) { MessageBox.Show("Now I got " + ex); } }
✓ VB.NET (ExceptionInThread), client-side code
Private Shared Sub response(ByVal handle As IAsyncResult) Dim theService As MyService = _ CType(handle.AsyncState, MyService) Try Dim result As Integer = theService.EndMethod1(handle) MessageBox.Show("Result received asynchronously " & _ result) Catch ex As System.Web.Services.Protocols.SoapException MessageBox.Show("Now I got " & ex.ToString()) End Try End Sub
Now when you make the call asynchronously, you get the message shown in Figure 7-25.
When using threads from the thread pool, pay extra attention to exception handling. Exceptions may not be reported, depending on what type of object you use: Delegate
or Timer
.
Gotcha #6, "Exceptions may go unhandled" and Gotcha #58, "Threads from the thread pool are scarce.”
Typically, you execute a task in a thread other than the main thread if that task might take a long time, but meanwhile you want your application to be responsive, plus you want to be able to preempt the task. Once it has completed, how do you display the results in the form’s control? Keep in mind that the form’s controls are not thread-safe. The only methods and properties of a control that are thread-safe are BeginInvoke()
, EndInvoke()
, Invoke()
, InvokeRequired
, and CreateGraphics()
. This is not a flaw. It was done by design to improve performance by eliminating thread-synchronization overhead. If you want to access any other methods or properties of a control, you should do so only from the thread that owns the control’s underlying window handle. This is typically the thread that created the control, which generally is the main thread in your application.
What happens if you access the non-thread-safe methods of a control from another thread? Unfortunately, the program may appear to work fine on several occasions. But just because the program runs, it does not mean it has no problems. In this case it is not a question of if, but of when the program will misbehave. These kinds of problems are difficult to predict and often may not be easily reproduced.
How can you call a method on a control from within another thread? You can do that using the System.Windows.Forms.Control.Invoke()
method on the control. This method executes a delegate on the thread that owns the control’s window handle. The call to Invoke()
blocks until that method completes.
Say you create a System.Windows.Forms.Timer
in a Form
. Should you access the controls of a Form
from within the Timer
’s callback method? What about from the callback of an asynchronous method call to a web service? The answers depend on understanding the thread in which each of these executes. Things become clearer when you examine the details.
An example will clarify this. Figure 7-26 shows a WinForm
application with three buttons and a couple of labels.
The Timer button is linked to code that creates a System.Windows.Forms.Timer
. It in turn executes a method, SetExecutingThreadLabel()
, that provides some details on the executing thread.
It is important to distinguish this Timer
, which resides in the System.Windows.Forms
namespace, from the Timer
class you’ve already seen, which is in System.Timers.Timers
. (See Gotcha #58, "Threads from the thread pool are scarce" and Gotcha #61, "Exceptions thrown from threads in the pool are lost.”)
The Timers Timer button is linked to a handler that creates a System.Timers.Timer
object. It raises an event that also executes SetExecutingThreadLabel()
. Finally, the Delegate
button is tied to a handler that creates a Delegate
. It then calls its BeginInvoke()
, which also executes SetExecutingThreadLabel()
. The code is shown in Examples 7-29 and 7-30.
Example 7-29. Executing thread for different Timers and Delegate (C#)
private void Form1_Load(object sender, System.EventArgs e) { MainThreadLabel.Text = AppDomain.GetCurrentThreadId().ToString(); } private delegate void SetLabelDelegate(string message); private void SetExecutingThreadLabel(string message) { ExecutingThreadLabel.Text = message; } private void TimerButton_Click( object sender, System.EventArgs e) { Timer theTimer = new Timer(); theTimer.Interval = 1000; theTimer.Tick += new EventHandler(Timer_Tick); theTimer.Start(); } private void Timer_Tick(object sender, EventArgs e) { Invoke(new SetLabelDelegate(SetExecutingThreadLabel), new object[] { "Timer : " + AppDomain.GetCurrentThreadId() + ": " + InvokeRequired }); (sender as Timer).Stop(); } private void TimersTimerButton_Click( object sender, System.EventArgs e) { System.Timers.Timer theTimer = new System.Timers.Timer(1000); theTimer.Elapsed += new System.Timers.ElapsedEventHandler( Timer_Elapsed); theTimer.Start(); } private void Timer_Elapsed( object sender, System.Timers.ElapsedEventArgs e) { Invoke(new SetLabelDelegate(SetExecutingThreadLabel), new object[] { "Timers.Timer : " + AppDomain.GetCurrentThreadId() + ": " + InvokeRequired }); (sender as System.Timers.Timer).Stop(); } private delegate void AsynchDelegate(); private void DelegateButton_Click( object sender, System.EventArgs e) { AsynchDelegate dlg = new AsynchDelegate(AsynchExecuted); dlg.BeginInvoke(null, null); } private void AsynchExecuted() { Invoke(new SetLabelDelegate(SetExecutingThreadLabel), new object[] { "Delegate : " + AppDomain.GetCurrentThreadId() + ": " + InvokeRequired }); }
Example 7-30. Executing thread for different Timers and Delegate (VB.NET)
Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load MainThreadLabel.Text _ = AppDomain.GetCurrentThreadId().ToString() End Sub Private Delegate Sub SetLabelDelegate(ByVal message As String) Private Sub SetExecutingThreadLabel(ByVal message As String) ExecutingThreadLabel.Text = message End Sub Private Sub TimerButton_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles TimerButton.Click Dim theTimer As Timer = New Timer theTimer.Interval = 1000 AddHandler theTimer.Tick, _ New EventHandler(AddressOf Timer_Tick) theTimer.Start() End Sub Private Sub Timer_Tick(ByVal sender As Object, _ ByVal e As EventArgs) Invoke(New SetLabelDelegate( _ AddressOf SetExecutingThreadLabel), _ New Object() { _ "Timer : " _ & AppDomain.GetCurrentThreadId() & ": " _ & InvokeRequired}) CType(sender, Timer).Stop() End Sub Private Sub TimersTimerButton_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles TimersTimerButton.Click Dim theTimer As System.Timers.Timer = _ New System.Timers.Timer(1000) AddHandler theTimer.Elapsed, _ New System.Timers.ElapsedEventHandler( _ AddressOf Timer_Elapsed) theTimer.Start() End Sub Private Sub Timer_Elapsed( _ ByVal sender As Object, _ ByVal e As System.Timers.ElapsedEventArgs) Invoke(New SetLabelDelegate( _ AddressOf SetExecutingThreadLabel), _ New Object() { _ "Timers.Timer : " _ & AppDomain.GetCurrentThreadId() & ": " _ & InvokeRequired}) CType(sender, System.Timers.Timer).Stop() End Sub Private Delegate Sub AsynchDelegate() Private Sub DelegateButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles DelegateButton.Click Dim dlg As AsynchDelegate _ = New AsynchDelegate(AddressOf AsynchExecuted) dlg.BeginInvoke(Nothing, Nothing) End Sub Private Sub AsynchExecuted() Invoke(New SetLabelDelegate( _ AddressOf SetExecutingThreadLabel), _ New Object() { _ "Delegate : " _ & AppDomain.GetCurrentThreadId() & ": " _ & InvokeRequired}) End Sub
Running the program and clicking on the buttons produces the results shown in Figures 7-27 through 7-29.
A few observations can be made from the above example and the related output:
The event executed by the System.Windows.Forms.Timer
runs in the main thread. So it is perfectly safe to access the controls directly from within this Timer
’s event handler. Be careful not to put any long-running code in this method. Otherwise, you will be holding up the main event-dispatch thread, and the performance and responsiveness of your application will suffer.
The System.Timers.Timer
’s event handler executes in a thread from the thread pool. From this thread, you should not interact with the controls directly, as it is not the thread that owns them. You have to use the System.Windows.Forms.Control.Invoke()
method.
The Delegate
’s BeginInvoke()
method calls the method from a thread pool thread as well. You should not access the controls directly from the invoked method either. Here, too, you have to call Invoke()
.
If you write an application that involves several threads, you can see how the above details can complicate your efforts. Further, forgetting them may lead to programs that misbehave. How can you ease these concerns?
One good way is to write a method that talks to the controls. Instead of accessing the controls from any random thread, call this method. It can easily check if it’s OK to access the controls directly, or if it should go through a delegate.
Let’s modify the code in Example 7-29 to illustrate this. From the three event-handler methods, call SetExecutingThreadLabel()
. In this method, check to see if the executing thread is the one that owns the control. This can be done using the control’s InvokeRequired
property.[3]
If the executing thread does own the control (InvokeRequired
is false
), then access it directly. Otherwise, SetExecutingThreadLabel()
uses the Invoke()
method to call itself, as shown in Example 7-31.
Example 7-31. Effectively addressing InvokeRequired issue
private void SetExecutingThreadLabel(string message) { if(ExecutingThreadLabel.InvokeRequired) { Invoke(new SetLabelDelegate(SetExecutingThreadLabel), new object[] {message}); } else { ExecutingThreadLabel.Text = message; } } private void Timer_Tick(object sender, EventArgs e) { (sender as Timer).Stop(); SetExecutingThreadLabel( "Timer : " + AppDomain.GetCurrentThreadId() + ": " + InvokeRequired); } private void Timer_Elapsed( object sender, System.Timers.ElapsedEventArgs e) { (sender as System.Timers.Timer).Stop(); SetExecutingThreadLabel( "Timer : " + AppDomain.GetCurrentThreadId() + ": " + InvokeRequired); } private void AsynchExecuted() { SetExecutingThreadLabel( "Delegate : " + AppDomain.GetCurrentThreadId() + ": " + InvokeRequired); }
Private Sub SetExecutingThreadLabel(ByVal message As String) If ExecutingThreadLabel.InvokeRequired Then Invoke(New SetLabelDelegate( _ AddressOf SetExecutingThreadLabel), _ New Object() {message}) Else ExecutingThreadLabel.Text = message End If Private Sub Timer_Tick(ByVal sender As Object, _ ByVal e As EventArgs) CType(sender, Timer).Stop() SetExecutingThreadLabel( _ "Timer : " _ & AppDomain.GetCurrentThreadId() & ": " _ & InvokeRequired) End Sub Private Sub Timer_Elapsed( _ ByVal sender As Object, _ ByVal e As System.Timers.ElapsedEventArgs) CType(sender, System.Timers.Timer).Stop() SetExecutingThreadLabel( _ "Timer : " _ & AppDomain.GetCurrentThreadId() & ": " _ & InvokeRequired) End Sub Private Sub AsynchExecuted() SetExecutingThreadLabel( _ "Delegate : " _ & AppDomain.GetCurrentThreadId() & ": " _ & InvokeRequired) End Sub
As you can see from this example, the methods executed by the various threads don’t have to worry about Invoke()
. They can simply call a method (SetExecutingThreadLabel()
in this case) to communicate with the control, and that method determines if it has to use Invoke()
. This approach not only makes it easier, it also helps deal with situations where you may inadvertently access controls from the wrong thread.
(The output screens are not shown here, because the change in the code does not affect them, except that the thread IDs are different.)
Understand which thread is executing your code. This is critical to decide if you can communicate with your controls directly, or if you should use the System.Windows.Forms.Control.Invoke()
method instead.
Gotcha #58, "Threads from the thread pool are scarce" and Gotcha #61, "Exceptions thrown from threads in the pool are lost.”
Say you are interested in making multiple independent requests to a web service that has a method with a delayed response. You can certainly take advantage of the asynchronous access that the service proxy generated by wsdl.exe/Visual Studio provides. However, if you want to make more than one request at the same time, should you use the same proxy instance or different instances?
Taking a closer look at the BeginInvoke()
and EndInvoke()
methods of the proxy, it is clear that when you call BeginInvoke()
, you are given an IAsyncResult
handle. When you call EndInvoke()
, you send this handle to it and get the result for that specific invocation. Doesn’t that suggest that you can make multiple asynchronous requests using a single instance of the proxy?
As it turns out, you can. However, you need to understand some issues from the web server’s point of view.
I have been asked this question quite a few times. I even ran into this situation inadvertently, and came to the realization illustrated in Examples 7-32 through 7-35.
Example 7-32. Multiple calls on a web service (C# server side)
✗ C# (MultipleWSCalls), server side
//MyService.asmx.cs part of ACSWSForMultiRequest.dll (Web Service) using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Web; using System.Web.Services; namespace ACSWSForMultiRequest { [WebService(Namespace="http://www.ACSWSForMultiRequest.com")] public class MyService : System.Web.Services.WebService { /// ... [WebMethod] public string Method1(int val) { System.Threading.Thread.Sleep(5000); return val.ToString(); } } }
Example 7-33. Multiple calls on a web service (C# client side)
✗ C# (MultipleWSCalls), client side
///Test.cs part of AClient.exe using System; using AClient.ACSWSForMultiRequest; namespace AClient { class Test { [STAThread] static void Main(string[] args) { MyService service = new MyService(); service.Credentials = System.Net.CredentialCache.DefaultCredentials; Console.WriteLine( "Making synchronous request at {0}", DateTime.Now.ToLongTimeString()); Console.WriteLine("Recevied {0} at {1}", service.Method1(0), DateTime.Now.ToLongTimeString()); Console.WriteLine("Making two requests at {0}", DateTime.Now.ToLongTimeString()); service.BeginMethod1(1, new AsyncCallback(display), service); service.BeginMethod1(2, new AsyncCallback(display), service); Console.ReadLine(); } private static void display(IAsyncResult handle) { MyService theService = handle.AsyncState as MyService; string result = theService.EndMethod1(handle); Console.WriteLine("Result {0} received {1}", result, DateTime.Now.ToLongTimeString()); } } }
Example 7-34. Multiple calls on a web service (VB.NET server side)
✗ VB.NET (MultipleWSCalls), server side
'MyService.asmx.vb part of AVBWSForMultiRequest.dll (Web Service) Imports System.Web.Services <System.Web.Services.WebService(Namespace := _ "http://tempuri.org/AVBWSForMultiRequest/MyService")> _ Public Class MyService Inherits System.Web.Services.WebService '... <WebMethod()> _ Public Function Method1(ByVal val As Integer) As String System.Threading.Thread.Sleep(5000) Return val.ToString() End Function End Class
Example 7-35. Multiple calls on a web service (VB.NET client side)
✗ VB.NET (MultipleWSCalls), client side
''Test.vb part of AClient.exe Imports AClient.AVSWSForMultiRequest Module Test Sub Main() Dim service As New MyService service.Credentials = _ System.Net.CredentialCache.DefaultCredentials Console.WriteLine( _ "Making synchronous request at {0}", _ DateTime.Now.ToLongTimeString()) Console.WriteLine("Recevied {0} at {1}", _ service.Method1(0), _ DateTime.Now.ToLongTimeString()) Console.WriteLine("Making two requests at {0}", _ DateTime.Now.ToLongTimeString()) service.BeginMethod1(1, _ New AsyncCallback(AddressOf display), service) service.BeginMethod1(2, _ New AsyncCallback(AddressOf display), service) Console.ReadLine() End Sub Private Sub display(ByVal handle As IAsyncResult) Dim theService As MyService = _ CType(handle.AsyncState, MyService) Dim result As String = theService.EndMethod1(handle) Console.WriteLine("Result {0} received {1}", _ result, DateTime.Now.ToLongTimeString()) End Sub End Module
In this example, the web service has a method Method1()
that has an artificial delay to simulate some activity. The client first makes one synchronous call, to which it gets a response after five seconds. (The reason you see an additional two-second delay on the first call is that the web service warms up when it receives the first request.) Then the client makes two asynchronous calls, one right after the other. The output from the above program is shown in Figure 7-30.
No problem—so far. Now, let’s say the service maintains state and expects the client code to set the proxy’s CookieContainer
property, as shown in Example 7-36.
Example 7-36. Making multiple calls when session is maintained
//MyService.asmx.cs part of ACSWSForMultiRequest.dll (Web Service) ... [WebMethod(true)] public string Method1(int val) { System.Threading.Thread.Sleep(5000); return val.ToString(); } ... //Test.cs. part of AClient.exe ... MyService service = new MyService(); service.CookieContainer = new System.Net.CookieContainer(); service.Credentials = System.Net.CredentialCache.DefaultCredentials; ...
✗ VB.NET (MultipleWSCalls)
'MyService.asmx.vb part of AVBWSForMultiRequest.dll (Web Service) ... <WebMethod(True)> _ Public Function Method1(ByVal val As Integer) As String System.Threading.Thread.Sleep(5000) Return val.ToString() End Function ... 'Test.vb part of AClient.exe ... Dim service As New MyService service.CookieContainer = _ New System.Net.CookieContainer service.Credentials = _ System.Net.CredentialCache.DefaultCredentials ...
In this code you have made one change to the web service. In the WebMethod
attribute, you have set the parameter (true
) to enable session management. You have also set the CookieContainer
property on the client proxy to a non-null
reference. Let’s run the application now and see what the response is. The output is shown in Figure 7-31.
Even though the two asynchronous requests are made simultaneously, the responses are separated by a five-second delay. This is because on the server side the requests get serialized. The web service recognizes the session id from the cookie that the proxy transmits and determines that the two requests belong to the same session. As a result, in order to avoid any threading conflicts, it serializes the requests, allowing only one request belonging to a session to execute at a time.
When you send multiple concurrent requests to a web service, do not use the same proxy instance if you set the CookieContainer
property on the proxy, or if session state is supported by the web service. Doing so causes the requests to execute consecutively on the server, rather than concurrently. If consecutive behavior is what you want, then this is a good approach.
Gotcha #58, "Threads from the thread pool are scarce" and Gotcha #61, "Exceptions thrown from threads in the pool are lost.”
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.
It worked this time, but you just got lucky—lucky 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
.
Unfortunately, RaiseEvent
is not thread-safe, as you can see from the MSIL in Figure 7-35.
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
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()
.
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.
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).
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.
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.
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.
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.”
18.117.170.226