Basic Instincts

Creating and Managing Secondary Threads

Contents

Creating a Thread
Passing Parameters to a Secondary Thread
When to Create New Threads
Conclusion

In my January 2004 column I showed you how to use delegates to execute a method in an asynchronous fashion. There you learned how to dispatch an asynchronous method call by simply calling BeginInvoke on a delegate object. Executing methods asynchronously using delegates is easy because the common language runtime (CLR) creates and manages a pool of worker threads for you behind the scenes. When you call BeginInvoke, the CLR takes care of dispatching your request to one of these worker threads from its built-in thread pool.

I recommend that you use asynchronous delegates and the programming techniques shown in my January 2004 column in the majority of cases where you need to execute code asynchronously from a desktop application. However, there are certain occasions when using asynchronous delegates doesn't provide as much flexibility or efficiency as you need.

This month I'll show you how to create and manage your own threads using the Thread class of the CLR. I'll also discuss the scenarios in which it's better to create and manage a secondary thread than to execute a method asynchronously with delegates.

Creating a Thread

When you want to create a new thread, you create an instance of the Thread class defined in the System.Threading namespace. The constructor of the Thread class accepts a reference to a ThreadStart delegate object. You can see two examples of how to create a new thread in the code in Figure 1.

Figure 1 Creating a Secondary Physical Thread

Imports System
Imports System.Threading

Class MyApp
  Shared Sub Main()

    '*** long-hand syntax for creating a thread
    Dim MyHandler As New ThreadStart(AddressOf TedsCode.MyAsyncTask)
    Dim ThreadA As New Thread(MyHandler)
    ThreadA.Start()

    '*** short-hand syntax for creating a thread
    Dim ThreadB As New Thread(AddressOf TedsCode.MyAsyncTask)
    ThreadB.Start()

  End Sub
End Class

Class TedsCode
  Shared Sub MyAsyncTask()
    '*** code to run on secondary thread
  End Sub
End Class

As you can see, there is a longhand syntax and a shorthand syntax for creating a new thread object. The technique for creating ThreadA in Figure 1 demonstrates how to explicitly create a ThreadStart delegate object that is bound to the method named MyAsyncTask. The technique for creating ThreadB accomplishes the same goal with less typing. That's because the Visual Basic® .NET compiler will implicitly create a ThreadStart delegate object when you write the following line of code:

Dim ThreadB As New Thread(AddressOf TedsCode.MyAsyncTask)

With version 1.0 and version 1.1 of the CLR, each Thread object is associated with its own physical Win32® thread. However, creating an object from the Thread class doesn't actually create a physical thread. Instead, you must call the Thread object's Start method for the CLR to call to the Windows® OS and create a physical thread. Note that future versions of the CLR are likely to provide an optimization whereby it will not be necessary to create a separate physical thread for each Thread object.

Once the CLR has created the physical thread, it uses it to execute the target method bound to the ThreadStart delegate object. The lifetime of this physical thread is controlled by the target method's execution. When execution of the method completes, the CLR gives control of the physical thread back to Windows. At this point, the OS destroys the physical thread.

You should realize that there is measurable overhead involved in creating and destroying physical threads. This overhead isn't all that significant in a desktop application where a thread is typically created in response to a user's action. However, creating new threads is something you should avoid in server-side code that's written for performance and scalability.

Remember that executing methods asynchronously using delegates leverages the built-in thread pool of the CLR. Therefore, achieving asynchronous execution using delegates scales better in a server-side application because it doesn't require the continual creation and destruction of physical threads.

Passing Parameters to a Secondary Thread

When you create a new Thread object to execute a method asynchronously, you must use a method that matches the calling signature of the ThreadStart delegate. That means you must create a method that is a Sub procedure that accepts no parameters (see the MyAsyncTask sub that is shown in Figure 1). This makes it a bit tricky to pass parameters to a method that is going to execute on a newly created thread.

There is a common technique you can use for passing parameters to a secondary thread; it involves creating a custom thread class. Examine the class definition for TedsThreadClass, shown in Figure 2. You can see how it has been designed with a custom set of instance fields and a parameterized constructor. When you create an object from a custom thread class such as TedsThreadClass, you can initialize it with whatever parameter values are required in your particular situation.

Figure 2 Passing Parameters to an Asynchronous Method

Imports System
Imports System.Threading

Class MyApp
  Shared Sub Main()

    Dim ThreadA As New TedsThreadClass(1, "Bob")
    ThreadA.Start()

    Dim ThreadB As New TedsThreadClass(2, "Betty")
    ThreadB.Start()

  End Sub
End Class

Class TedsThreadClass

  '*** define custom fields and constructor with parameterized data
  Protected x As Integer
  Protected y As String
  Sub New(ByVal X As Integer, ByVal Y As String)
    Me.x = X
    Me.y = Y
  End Sub

  '*** instance method used to run asynchronous task
  Sub MyAsyncTask()
    '*** this code has access to x and y
  End Sub

  '*** encapsulate threading support
  Protected InnerThread As New Thread(AddressOf Me.MyAsyncTask)
  Public Sub Start()
    InnerThread.Start()
  End Sub

End Class

Note that TedsThreadClass is different from the example shown in Figure 1 because in Figure 2 the MyAsyncTask method is defined as an instance method instead of a shared method. That means that the MyAsyncTask method in Figure 2 can access the custom instance fields x and y. The key point of this technique is that it provides the means for passing a customized set of parameters to a method executing on a new secondary thread.

TedsThreadClass contains a protected field named InnerThread, which holds a Thread object that's bound to the instance method MyAsyncTask and a public Start method. This allows the custom thread class to encapsulate the implementation details for creating and managing Thread objects. To use this custom thread class, simply instantiate an object and call its Start method, like this:

Dim ThreadA As New TedsThreadClass(1, "Bob")
ThreadA.Start()

You have just seen what's involved with a custom thread class that's been designed to pass parameters to an asynchronous method call. Remember that delegates make passing parameters in an asynchronous method call much easier because you can define a custom delegate type with whatever parameter list you like. Note that making an asynchronous method call by creating and managing a thread usually requires more work during design and coding.

You should also consider that executing a method asynchronously with delegates provides an easy way to harvest the return value and output parameters: the delegate's EndInvoke method. Executing methods asynchronously with a new Thread object is not as convenient. You must devise a custom scheme to pass data from secondary threads back to the application's primary thread.

When you are creating and managing secondary threads, it also takes more effort to coordinate thread synchronization. For example, what should you do when you need the application's primary thread to wait until a secondary thread has completed its work? You should call the Join method supplied by the Thread class, as shown in the following code:

Dim ThreadA As Thread
ThreadA = New Thread(AddressOf TedsCode.MyAsyncTask)

'*** do some work on primary thread

'*** now block until secondary thread is done
ThreadA.Join()

The Join method is a blocking call. The thread that calls Join is blocked until the other thread has finished its work. In other words, the Join method is used to take two threads representing parallel paths of execution and join them together into a single path.

When to Create New Threads

I have already outlined several reasons why executing a method asynchronously on a new Thread object is more difficult and not as efficient as using an asynchronous delegate. Because asynchronous delegates are easier to use and they provide the efficiency of using the built-in thread pool, I recommend you use them when you have to make asynchronous method calls.

There are, however, a few design situations in which delegates should not be used. The following is a list of circumstances in which you should most likely use secondary threads rather than delegates for asynchronous execution.

  • You need to execute a long-running task
  • You need to adjust a thread's priority
  • You need a foreground thread that will keep a managed desktop application alive
  • You need a single-threaded apartment (STA) thread to work with apartment-threaded COM objects

When you need to dedicate a thread to a task that's going to take a long time, you should create a new thread. For example, imagine you need to dedicate a secondary thread to watch for updates to a file or to listen for incoming data on a network socket, and you need it for the lifetime of the application. It would be considered bad style to use delegates because you would effectively be taking a thread out of the CLR thread pool and never returning it. Asynchronous method execution using delegates should only be used for relatively short-running tasks.

When you need to change a thread's priority, you should create a new thread. The Thread class exposes a public Priority property that allows you to increase or decrease the priority of a thread. However, you should not change the priority of threads from the CLR thread pool. You should only adjust the priority of the threads you have created by calling New on the Thread class.

Let's look at a simple example involving a desktop application. Imagine you want to run an asynchronous method on a secondary thread with a lower priority to reduce its impact on the responsiveness of the application's user interface. You can adjust the Thread object's Priority property before calling the Start method:

Dim ThreadA As Thread
ThreadA = New Thread(AddressOf TedsCode.MyAsyncTask)
ThreadA.Priority = ThreadPriority.BelowNormal
ThreadA.Start()

The settings for a thread's priority level are Highest, AboveNormal, Normal, BelowNormal, and Lowest. You should generally only lower thread priorities and try to refrain from raising them. Be careful of choosing the Highest and AboveNormal settings because they can have unpredictable effects on the application and the system as a whole.

Creating a new thread is also a good idea when you want to keep a managed desktop application alive while a secondary thread is running. Imagine a scenario in a Windows Forms application where the user closes the main form while a secondary thread is performing a task in the background. Is the fact that this secondary thread is still running important enough to keep the application alive? If the secondary thread is a background thread, the answer is no. The application will shut down right away. However, if the secondary thread is a not a background thread, the app will keep running.

Each Thread object has an IsBackground property that indicates whether it is a background thread. All the threads in the CLR thread pool are background threads and should not be modified in this respect. That means a task executing asynchronously as a result of a call to BeginInvoke on a delegate is never important enough by itself to keep an application alive.

When you create a new Thread object, it is by default a foreground thread because its IsBackground property is set to false. That means your application will continue to run while a secondary thread is executing. If you would like to create a new thread and make it a background thread, you assign a value of True to its IsBackground property before you call the Start method, like so:

Dim ThreadA As Thread
ThreadA = New Thread(AddressOf TedsCode.MyAsyncTask)
ThreadA.IsBackground = True
ThreadA.Start()

Remember that whether a Thread object is a foreground or background thread is really only important when writing managed EXE-based applications such as a console application or a Windows Forms application. The IsBackground property of a Thread object has no effect on applications that have been started using a non-managed EXE such as the ASP.NET worker process.

Finally, when you need to initialize a thread for COM interoperability to run within a single-threaded apartment you should create a new thread. To explain why this is important, I'll provide a little background on COM.

In COM, there are STAs and multithreaded apartment (MTA) threads. The majority of COM components, including all those created with Visual Basic 5.0 and Visual Basic 6.0, are apartment-threaded. Apartment-threaded components are restricted to running on STA threads due to issues related to thread safety and affinity. A small percentage of performance-oriented COM components have been written to run safely on MTA threads.

Before code written with Visual Basic .NET can create and interact with a COM object, the CLR must first initialize the calling thread for COM interoperability. In particular, the CLR must make a system call to initialize the thread as either MTA or STA.

If you create apartment-threaded objects using an MTA thread, you will experience thread switching. Thread switching involves a measurable performance hit because each call to the apartment-threaded COM object must be marshaled from the MTA thread over to the object's STA thread. Thread switching can be avoided by using an STA thread instead of an MTA thread. Therefore, you should always use STA threads when creating objects from apartment-threaded COM components.

By default, the primary thread for an application created with Visual Basic .NET will initialize to an STA thread. When you compile a Windows Forms app or a console app, the Visual Basic .NET compiler automatically adds the STAThread attribute to the Main method that serves as the application's entry point:

Class MyApp
  <System.STAThread()> Shared Sub Main()
    '*** application code here
  End Sub
End Class

The presence of the STAThread attribute forces the application's primary thread to initialize as an STA thread the first time the application does any interoperability work with COM. This is what you want when working with COM components because it is likely that they will be apartment-threaded. In rare cases when you need to write a Visual Basic .NET-based application that uses COM components that are all freethreaded, you can force the application's primary thread to initialize to an MTA thread by explicitly adding the MTAThread attribute to the application's Main method:

Class MyApp
  <System.MTAThread()> _
  Shared Sub Main()
    '*** application code here
  End Sub
End Class

I've just discussed the issues that affect whether an application's primary thread gets initialized as an STA or an MTA thread. Now it's time to look into how things work with secondary threads. The first important point to understand is that you have no control over the threads from the CLR thread pool. They are always initialized as MTA threads. For this reason, you should avoid using asynchronous delegates to execute code which interoperates with apartment-threaded COM components due to the performance degradation that occurs with thread switching.

When you create and manage a secondary thread that is going to interoperate with COM components, you can control whether it is initialized as an STA thread or an MTA thread. After you create an object from the Thread class, you can explicitly set its ApartmentState property to STA before calling Start, as you see here:

Dim ThreadA As Thread
ThreadA = New Thread(AddressOf TedsCode.MyAsyncTask)
ThreadA.ApartmentState = ApartmentState.STA
ThreadA.Start()

'*** When you create a COM object, the CLR 
'*** will initialize ThreadA as an STA thread.

You now have a secondary thread which can create and make calls to apartment-threaded COM objects without incurring the overhead of thread switching. This makes it possible to interact with apartment-threaded COM components from a secondary thread in a more efficient manner.

Conclusion

This month I have examined why and how you would create secondary threads. You saw that creating and managing secondary threads to execute methods asynchronously is something you should do only when you cannot use delegates. Such situations include handling long-running tasks, adjusting thread priority, using foreground threads, and using STA threads for more efficient interoperability with apartment-threaded COM components.

My last three columns have examined how to use asynchronous delegates and how to create secondary threads. In short, I've taught you how to get in trouble by introducing multithreaded behavior in your apps. In the next installment of Basic Instincts, I'll show you how to stay out of trouble by designing and writing thread-safe code that is robust in the face of concurrency and that can run dependably in a multithreaded world.

Send your questions and comments for Ted to  instinct@microsoft.com.

Ted Pattison is a cofounder of Barracuda .NET, a developer education company that assists companies building collaborative applications using Microsoft technologies. Ted is the author of several books including Building Applications and Components with Visual Basic .NET (Addison-Wesley, 2003).