GOTCHA #60 Passing parameters to threads is trickySay 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 threadC# (ParamThreadSafety) //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); } } } VB.NET (ParamThreadSafety) '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 parameterC# (ParamThreadSafety) //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... } } } VB.NET (ParamThreadSafety) '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. Figure 7-18. Output from Example 7-18See, 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-18C# (ParamThreadSafety) 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. Figure 7-19. Output from Example 7-19As 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 parameterC# (ParamThreadSafety) //... 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... } VB.NET (ParamThreadSafety) '... 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. Figure 7-20. Output from Example 7-20In 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 ThreadRunnerC# (ParamThreadSafety) //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... } } } VB.NET (ParamThreadSafety) '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 classC# (ParamThreadSafety) //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); } } } VB.NET (ParamThreadSafety) '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. IN A NUTSHELLWhen you start a thread with a method that requires parameters, wrapper classes like ThreadRunner (see Example 7-22) can help ensure thread safety. SEE ALSOGotcha #59, "Threads invoked using delegates behave like background threads." |