GOTCHA 60 Passing parameters to threads is tricky


GOTCHA #60 Passing parameters to threads is tricky

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

C# (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 parameter

C# (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-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

C# (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-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

C# (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-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

C# (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 class

C# (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 NUTSHELL

When you start a thread with a method that requires parameters, wrapper classes like ThreadRunner (see Example 7-22) can help ensure thread safety.

SEE ALSO

Gotcha #59, "Threads invoked using delegates behave like background threads."



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

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