In the pre–Microsoft .NET world, Visual Basic developers had to jump through extraordinary hurdles to use threads and often wound up with corrupted data or a crashed integrated development environment (IDE) for their trouble. In Microsoft Visual Basic .NET, working with threads is as easy as creating an object and calling a method. However, multithreading safely and efficiently isn't as straightforward.
This chapter presents a few basic patterns that can help you design a robust multithreading framework. You'll see basic recipes for calling a method asynchronously through a delegate (recipes 7.1 to 7.4) and with the Thread class (recipes 7.5 to 7.8), and learn how to update a Windows user interface from a background thread (7.9). You'll also see how to create the popular thread wrapper class (7.11) and build a continuous task processor (7.12).
Note |
The recipes in this chapter aren't a replacement for a dedicated tutorial about multithreading. If you haven't written multithreaded code before, you should start with a more general introduction. Both Programming Microsoft Visual Basic .NET Core Reference (Microsoft Press, 2002) and my own The Book of VB .NET (No Starch Press, 2002) can provide an excellent introduction. |
Developers usually add multithreading to an application in order to handle multiple tasks at the same time (for example, a server that might need to interact with several clients simultaneously), add responsiveness, or reduce wait times for short tasks that might otherwise be held up by other, more intensive work. In order to simulate long tasks, many of the recipes in this chapter use the Delay method shown here:
Private Function Delay(ByVal seconds As Integer) As Integer Dim StartTime As DateTime = DateTime.Now Do ' Do nothing. Loop While DateTime.Now.Subtract(StartTime).TotalSeconds < seconds Return seconds End Function
This function simply waits the indicated amount of time and then returns the number of seconds it waited. The shared Thread.Sleep method could also be used with the same effect, but I've used this approach because it simulates code executing (rather than simply pausing the thread temporarily). The method returns the wait time because this allows the recipes to demonstrate another important concept—retrieving data from a separate thread. This task isn't as straightforward as it is when calling a function synchronously.
Note |
The .NET Framework includes types for multithreading programming in the System.Threading namespace. The recipes in this chapter assume you have imported this namespace. |
You want a method to execute on another thread, so your program can continue with other tasks.
Create a delegate for the method. You can then use the BeginInvoke and EndInvoke methods.
The .NET Framework includes support for delegates, which act like type-safe function pointers. Using a delegate, you can store a reference to a procedure and then invoke it through the delegate. This process is similar to the way you might invoke a class method through an interface.
Behind the scenes, delegates are actually dynamically generated classes. As part of their functionality, they contain the ability to call the referenced method synchronously or asynchronously, so that it won't block your code. To invoke a delegate asynchronously, you use the BeginInvoke method. You can retrieve the result later using the EndInvoke method.
For example, imagine you have the following function definition:
Private Function TimeConsumingTaskA(ByVal seconds As Integer) As Integer ' (Code omitted.) End Function
A delegate for this function would define the exact same signature. That means that parameters and return value must be the same in number and data type, although the names are not important.
Private Delegate Function TimeConsumingTask(ByVal sec As Integer) As Integer
Now you can invoke TimeConsumingTaskA through the TimeConsumingTask delegate synchronously
' Create an instance of the delegate. Dim Invoker As New TimeConsumingTask(AddressOf TimeConsumingTaskA) ' Invoke the delegate synchronously. ' This is equivalent to: TimeConsumingTaskA(30) Invoker(30)
or asynchronously
' Create an instance of the delegate. Dim Invoker As New TimeConsumingTask(AddressOf TimeConsumingTaskA) ' Invoke the delegate asynchronously. Dim AsyncResult As IAsyncResult AsyncResult = Invoker.BeginInvoke(30, Nothing, Nothing) ' (Do something else here.) ' Retrieve the results from the asynchronous call. ' If it is not complete, this call effectively makes it synchronous, ' and waits for it to complete. Dim Result As Integer = Invoker.EndInvoke(AsyncResult)
Notice that when using BeginInvoke, the ordinary function parameters come first, followed by two additional parameters, which allow you to specify a callback and state information (see recipe 7.4). You can create a delegate to asynchronously call any method, whether it's shared, public, private, or even belongs to a Web service or .NET Remoting proxy. You can also check if a delegate call has completed using the returned IAsyncResult object, which provides a Boolean IsCompleted property.
Note |
If you make an asynchronous call to a procedure that does not return any information, you won't need to call EndInvoke. However, it's still recommended for error handling. If an unhandled exception occurs while the asynchronous method is executing, you won't be notified until you call EndInvoke. Thus, if you need to add any exception-handling logic, you should add it to the EndInvoke call, not BeginInvoke. |
Following is a full example that demonstrates two tasks that execute at the same time using an asynchronous call through a delegate. Each task delays 30 seconds, but the total delay is dramatically reduced.
Public Module AsynchronousInvoke Private Delegate Function TimeConsumingTask(ByVal seconds As Integer) _ As Integer Public Sub Main() Dim StartTime As DateTime = DateTime.Now Dim ResultA, ResultB As Integer ' Start the first task on a new thread. ' Specify a delay of 30 seconds. Dim AsyncInvoker As New TimeConsumingTask( _ AddressOf TimeConsumingTaskA) Dim AsyncResult As IAsyncResult AsyncResult = AsyncInvoker.BeginInvoke(30, Nothing, Nothing) ' Start the second task on the main thread. ' Specify a delay of 30 seconds. ' This blocks until complete. ResultB = TimeConsumingTaskB(30) ' Retrieve the result of the asynchronous tasks. ' If it is not already complete, this blocks until complete. ResultA = AsyncInvoker.EndInvoke(AsyncResult) Console.WriteLine("Method A delayed for: " & ResultA.ToString()) Console.WriteLine("Method B delayed for: " & ResultB.ToString()) Console.WriteLine("Total seconds taken to execute: " & _ DateTime.Now.Subtract(StartTime).TotalSeconds.ToString()) Console.ReadLine() End Sub Private Function TimeConsumingTaskA(ByVal seconds As Integer) As Integer Return Delay(seconds) End Function Private Function TimeConsumingTaskB(ByVal seconds As Integer) As Integer Return Delay(seconds) End Function ' (Delay function omitted.) End Module
Here's the output you should see:
Method A delayed for: 30 Method B delayed for: 30 Total seconds taken to execute: 30.894424
The delegate approach masks much of the complexity of threading because you don't need to worry about marshalling data. However, if you call more than one delegate that interact with the same piece of data (like a form-level variable), you will need to add locking code, or a conflict could occur.
Note |
Although simple, delegates also have certain limitations that the Thread class does not. For example, you can't control the priority of an asynchronous delegate thread, and you are limited in the number of asynchronous calls that will actually execute asynchronously (depending on how many threads the common language runtime makes available in its pool). |
You want to call multiple procedures asynchronously and suspend further processing until they are all complete.
Retrieve the WaitHandle for each call, and use the shared WaitHandle.WaitAll method.
When you call the BeginInvoke method, you receive an IAsyncResult object that allows you to check the status of the thread and complete the request. In addition, the IAsyncResult interface defines an AsyncWaitHandle property that allows you to retrieve a WaitHandle for the asynchronous request.
The WaitHandle class defines three methods: WaitAll, WaitAny, and WaitOne. You can use the shared WaitAll method with an array of WaitHandle objects to wait for a group of asynchronous tasks to complete.
The following code shows an example that starts three tasks (taking 10, 30, and 15 seconds, respectively). The WaitAll method will return after all tasks have completed, in approximately 30 seconds.
Public Module AsynchronousInvoke Private Delegate Function TimeConsumingTask(ByVal seconds As Integer) _ As Integer ' The WaitAll() method must execute on a MTA thread. _ Public Sub Main() Dim ResultA, ResultB, ResultC As Integer ' Define and start three tasks. Dim AsyncInvoker As New TimeConsumingTask(AddressOf Delay) Dim AsyncResultA, AsyncResultB, AsyncResultC As IAsyncResult AsyncResultA = AsyncInvoker.BeginInvoke(10, Nothing, Nothing) AsyncResultB = AsyncInvoker.BeginInvoke(30, Nothing, Nothing) AsyncResultC = AsyncInvoker.BeginInvoke(15, Nothing, Nothing) Console.WriteLine("Call A is: " & AsyncResultA.IsCompleted.ToString()) Console.WriteLine("Call B is: " & AsyncResultB.IsCompleted.ToString()) Console.WriteLine("Call C is: " & AsyncResultC.IsCompleted.ToString()) ' Block until all tasks are complete. Console.WriteLine("Waiting...") Dim WaitHandles() As WaitHandle = {AsyncResultA.AsyncWaitHandle, _ AsyncResultB.AsyncWaitHandle, AsyncResultC.AsyncWaitHandle} WaitHandle.WaitAll(WaitHandles) Console.WriteLine("Call A is: " & AsyncResultA.IsCompleted.ToString()) Console.WriteLine("Call B is: " & AsyncResultB.IsCompleted.ToString()) Console.WriteLine("Call C is: " & AsyncResultC.IsCompleted.ToString()) Console.ReadLine() End Sub ' (Delay function omitted.) End Module
You want to call multiple procedures asynchronously and suspend processing until any one call completes.
Retrieve the WaitHandle for each call, and use the shared WaitHandle.WaitAny method.
The System.Threading.WaitHandle class provides a WaitAny method that accepts an array of WaitHandle objects and blocks until at least one WaitHandle is completed. When WaitAny returns, it provides an index number that indicates the position of the completed WaitHandle in the array.
The following example launches three calls at once. It then waits until at least one call finishes, displays the results, and then resumes waiting for one of the next two calls to complete. It uses an ArrayList to manage the WaitHandle objects, removing them as they are completed. The ArrayList is copied to a strongly typed array just before the WaitAny call is made.
Public Module AsynchronousInvoke Private Delegate Function TimeConsumingTask(ByVal seconds As Integer) _ As Integer ' The WaitAny() method must execute on a MTA thread. _ Public Sub Main() Dim ResultA, ResultB, ResultC As Integer ' Define and start three tasks. Dim AsyncInvoker As New TimeConsumingTask(AddressOf Delay) Dim AsyncResultA, AsyncResultB, AsyncResultC As IAsyncResult AsyncResultA = AsyncInvoker.BeginInvoke(10, Nothing, Nothing) AsyncResultB = AsyncInvoker.BeginInvoke(30, Nothing, Nothing) AsyncResultC = AsyncInvoker.BeginInvoke(15, Nothing, Nothing) Dim WaitHandles() As WaitHandle Dim WaitHandleList As New ArrayList() WaitHandleList.Add(AsyncResultA.AsyncWaitHandle) WaitHandleList.Add(AsyncResultB.AsyncWaitHandle) WaitHandleList.Add(AsyncResultC.AsyncWaitHandle) Dim StartTime As DateTime = DateTime.Now Do WaitHandles = CType( _ WaitHandleList.ToArray(GetType(WaitHandle)), WaitHandle()) ' Block until at least one request is complete. Console.WriteLine("Waiting...") Dim CompletedIndex As Integer = WaitHandle.WaitAny(WaitHandles) WaitHandleList.RemoveAt(CompletedIndex) ' Display the current status. Console.WriteLine( _ DateTime.Now.Subtract(StartTime).TotalSeconds.ToString() & _ " seconds elapsed.") Console.WriteLine("Call A is: " & _ AsyncResultA.IsCompleted.ToString()) Console.WriteLine("Call B is: " & _ AsyncResultB.IsCompleted.ToString()) Console.WriteLine("Call C is: " & _ AsyncResultC.IsCompleted.ToString()) Loop Until WaitHandleList.Count = 0 Console.WriteLine("Completed.") Console.ReadLine() End Sub ' (Delay function omitted.) End Module
The results will appear as follows:
Waiting... 10.1445872 seconds elapsed. Call A is: True Call B is: False Call C is: False Waiting... 18.6868704 seconds elapsed. Call A is: True Call B is: False Call C is: True Waiting... 31.0746832 seconds elapsed. Call A is: True Call B is: True Call C is: True Completed.
You want to be notified as soon as an asynchronous call completes, without needing to poll the IAsyncResult object.
Use a callback, which will be triggered automatically when the call completes.
Often, when you start a multithreaded task in an application, you don't want the additional complexity and overhead of monitoring that task. The solution is to use a callback. With a callback, your code executes a function or subroutine asynchronously and carries on with other work. Your callback procedure is automatically invoked when the asynchronous call is finished.
The callback approach allows you to separate the code that processes asynchronous tasks from the code that performs work on the main thread. However, it also works best if the work performed by the main thread is independent from the work you are performing asynchronously. One example might be a desktop application that downloads new status information from a Web service and updates the display periodically.
To use a callback, you simply pass the address of the callback procedure you want to use to the BeginInvoke method.
AsyncInvoker As New TimeConsumingTask(AddressOf Delay) ' Invoke the method and supply a callback. ' Note that the code does not retain the IAsyncResult object that is returned ' from BeginInvoke() because it is not needed. AsyncInvoker.BeginInvoke(10, AddressOf Callback, Nothing)
The callback procedure must have the signature defined by the System.AsyncCallback delegate. This includes a single parameter: the IAsyncResult object for the asynchronous call. Typically, you'll use this to call EndInvoke to retrieve the result from the delegate.
Private Sub Callback(ByVal ar As IAsyncResult) ' Retrieve the result for the call that just ended. Dim Result As Integer = AsyncInvoker.EndInvoke(ar) ' Display the result. Console.WriteLine(Result.ToString()) End Sub
In this example, AsyncInvoker is retained as a class member variable. That means that it can be conveniently accessed in the callback procedure. However, this design won't work if you want to make multiple asynchronous calls at once and handle them all using the same callback. In this case, you need to use the second optional BeginInvoke parameter to supply a custom state object.
For example, consider a time-consuming method that looks up weather readings from a database based on the city name. You might choose to use asynchronous calls so that the user can submit multiple city name queries at once. When a query completes, the callback runs. At this point, you need to be able to retrieve the original delegate to complete the call, and you need to retrieve information so you can match the answer (the temperature reading) with the original question (the city name). The easiest solution is to create a custom class that encapsulates these two pieces of information:
Public Class GetWeatherAsyncCallInfo Private _AsyncInvoker As GetWeatherDelegate Private _CityName As String Public Property AsyncInvoker() As GetWeatherDelegate Get Return _AsyncInvoker End Get Set(ByVal Value As GetWeatherDelegate) _AsyncInvoker = Value End Set End Property Public Property CityName() As String Get Return _CityName End Get Set(ByVal Value As String) _CityName = Value End Set End Property Public Sub New(ByVal [delegate] As GetWeatherDelegate, _ ByVal cityQuery As String) Me.AsyncInvoker = [delegate] Me.CityName = cityQuery End Sub End Class
Now, you can create a GetWeatherAsyncCallInfo object and pass it to the BeginInvoke method. It will then be returned in the callback procedure. Here is a complete Console application that demonstrates how this works.
Public Module AsynchronousInvoke Public Delegate Function GetWeatherDelegate(ByVal cityName As String) _ As Single Public Sub Main() ' Define and start three tasks. Dim AsyncInvokerA As New GetWeatherDelegate(AddressOf GetWeather) Dim QueryA As New GetWeatherAsyncCallInfo(AsyncInvokerA, "New York") AsyncInvokerA.BeginInvoke("New York", AddressOf Callback, QueryA) Dim AsyncInvokerB As New GetWeatherDelegate(AddressOf GetWeather) Dim QueryB As New GetWeatherAsyncCallInfo(AsyncInvokerB, "Tokyo") AsyncInvokerB.BeginInvoke("Tokyo", AddressOf Callback, QueryB) Dim AsyncInvokerC As New GetWeatherDelegate(AddressOf GetWeather) Dim QueryC As New GetWeatherAsyncCallInfo(AsyncInvokerC, "Montreal") AsyncInvokerC.BeginInvoke("Montreal", AddressOf Callback, QueryC) Console.WriteLine("Waiting... press Enter to exit.") Console.ReadLine() End Sub Private Function GetWeather(ByVal cityName As String) As Single ' This code would query the database for the requested info. ' Instead, we pause and return a random "temperature reading." Dim Rand As New Random() Delay(Rand.Next(5, 10)) Return Rand.Next(30, 100) End Function Private Sub Callback(ByVal ar As IAsyncResult) ' Retrieve the state object. Dim QueryInfo As GetWeatherAsyncCallInfo = CType(ar.AsyncState, _ GetWeatherAsyncCallInfo) ' Complete the call and retrieve the result. Dim Result As Single = QueryInfo.AsyncInvoker.EndInvoke(ar) Console.WriteLine("Result for: " & QueryInfo.CityName & _ " is: " & Result.ToString()) End Sub ' (Delay function omitted.) End Module
The output for a test run is as follows:
Waiting... press any key to exit. Result for: Tokyo is: 47 Result for: New York is: 75 Result for: Montreal is: 51
Notice that the callback will execute on the same thread as the asynchronous task, which is not the main thread of the application. This means that if the callback accesses a class member variable, it needs to use locking code (as shown in recipe 7.6). Similarly, if the callback accesses a portion of a Windows user interface, it needs to marshal the call to the correct thread (see recipe 7.9). In this case, the callback accesses the Console object, which is known to be thread-safe, and so no locking code is required.
You want to execute a task on another thread and be able to control that thread's priority, state, and so on.
Create a new System.Threading.Thread object that references the code you want to execute asynchronously, and use the Start method.
The Thread object allows you to write multithreaded code with a finer degree of control than that provided by asynchronous delegates. As with delegates, the Thread class can only point to a single method. However, unlike delegates, the Thread class can only execute a method that takes no parameters and has no return value. (In other words, it must match the signature of the System.Threading.ThreadStart delegate.) If you need to send data or instructions to a thread, or retrieve data from a thread, you will have to use a custom-threaded object, as shown in recipes 7.7 and 7.8.
The following example starts two threads, both of which iterate through a loop and write to the Console object. Because Console is thread-safe, no locking is required. If the user presses a key before the threads have finished processing, the threads are automatically aborted.
Public Module ThreadTest Public Sub Main() ' Define threads and point to code. Dim ThreadA As New Thread(AddressOf Task) Dim ThreadB As New Thread(AddressOf Task) ' Name the threads (aids debugging). ThreadA.Name = "Thread A" ThreadB.Name = "Thread B" ' Start threads. ThreadA.Start() ThreadB.Start() Console.WriteLine("Press Enter to exit...") Console.ReadLine() ' If the threads aren't yet finished, abort them. If (ThreadA.ThreadState And ThreadState.Running) = _ ThreadState.Running Then ThreadA.Abort() End If If (ThreadB.ThreadState And ThreadState.Running) = _ ThreadState.Running Then ThreadB.Abort() End If End Sub Private Sub Task() Dim i As Integer For i = 1 To 5 Console.WriteLine(Thread.CurrentThread.Name & _ " at: " & i.ToString) Delay(1) Next Console.WriteLine(Thread.CurrentThread.Name & " completed") End Sub ' (Delay function omitted.) End Module
Notice that this code tests the state of the threads using a bitwise And operation. That's because the ThreadState property can include multiple ThreadState values, and the code needs to filter out the one it wants to test for.
The output for this example looks like this:
Press any key to exit... Thread A at: 1 Thread B at: 1 Thread A at: 2 Thread B at: 2 Thread A at: 3 Thread B at: 3 Thread A at: 4 Thread B at: 4 Thread A at: 5 Thread B at: 5 Thread A completed Thread B completed
Threads support a variety of properties, including the following:
Threads also support a few basic methods:
Note |
Threads cannot be reused. Once a thread finishes its task, its state changes permanently to ThreadState.Stopped. You cannot call Start on the thread again. If you want to create a continuously executing, reusable thread, you need to add a loop, as described in recipe 7.12. |
You need to access the same object from multiple threads, without causing a concurrency problem.
Use the SyncLock statement to gain exclusive access to the object.
Trying to access an object on more than one thread at once is inherently dangerous, because a single command in a high-level language like Visual Basic .NET might actually compile to dozens of low-level machine language instructions. If two threads try to modify data at the same time, there's a very real possibility that the changes made by one thread will be overwritten—and this is only one of many possible threading errors, all of which occur sporadically and are extremely difficult to diagnose.
To overcome these limitations, you can use locking, which allows you to obtain exclusive access to an object. You can then safely modify the object without worrying that another thread might try to read or change it while your operation is in progress. Locking is built into Visual Basic .NET through the SyncLock statement. SyncLock works with any reference object (value types such as integers are not supported, so they must be wrapped in a class).
For example, consider the thread-safe counter object shown in the following code. It automatically locks itself before a change is made. Thus, multiple threads can call Increment at the same time without any danger that their changes will collide.
Public Class Counter Private _Value As Integer Public ReadOnly Property Value() As Integer Get Return _Value End Get End Property Public Sub Increment() SyncLock Me _Value += 1 End SyncLock End Sub End Class
Here's a Console application that demonstrates how multiple threads can use the same counter:
Public Module ThreadTest Private Counter As New Counter() Public Sub Main() ' Define threads and point to code. Dim ThreadA As New Thread(AddressOf Task) Dim ThreadB As New Thread(AddressOf Task) ' Name the threads (aids debugging). ThreadA.Name = "Thread A" ThreadB.Name = "Thread B" ' Start threads. ThreadA.Start() ThreadB.Start() Console.WriteLine("Press Enter to exit...") Console.ReadLine() ' If the threads aren't yet finished, abort them. If (ThreadA.ThreadState And ThreadState.Running) = _ ThreadState.Running Then ThreadA.Abort() End If If (ThreadB.ThreadState And ThreadState.Running) = _ ThreadState.Running Then ThreadB.Abort() End If End Sub Private Sub Task() Dim i As Integer For i = 1 To 5 Delay(1) Counter.Increment() Console.WriteLine(Thread.CurrentThread.Name & _ " set Counter.Value = " & Counter.Value.ToString()) Next End Sub ' (Delay function omitted.) End Module
The output for this example is as follows:
Press any key to exit... Thread A set Counter.Value = 1 Thread B set Counter.Value = 2 Thread A set Counter.Value = 3 Thread B set Counter.Value = 4 Thread B set Counter.Value = 5 Thread A set Counter.Value = 6 Thread A set Counter.Value = 7 Thread B set Counter.Value = 8 Thread A set Counter.Value = 9 Thread B set Counter.Value = 10
Be Careful with Locks
When you lock an object, all other threads that attempt to access it are put into a waiting queue. Thus, you should always hold locks for as short a time as possible, to prevent long delays and possible timeout errors.
In addition, using the SyncLock statement indiscriminately can lead to problems if you attempt to acquire multiple locks at once. For example, consider the following two methods:
Public Sub MethodA() SyncLock ObjectA SyncLock ObjectB ' (Do something with A and B). End SyncLock End SyncLock End Sub Public Sub MethodB() SyncLock ObjectB SyncLock ObjectA ' (Do something with A and B). End SyncLock End SyncLock End Sub
Assume MethodA gets a hold of ObjectA and then tries to obtain a lock of ObjectB. In the meantime, MethodB obtains exclusive access to ObjectB and tries for ObjectA. Both methods will wait for each other to release an object, with neither method giving in. This conflict is known as a deadlock, and it's a good reason to make your threading code as simple as possible. In addition, you might want to consider more advanced locking through the Monitor class, which allows you to test if an exclusive lock can be made and specify a timeout period for attempting to acquire a lock.
You want to execute a task on a separate thread, and you need to supply certain input parameters.
Create a custom threaded class that incorporates a parameterless method, along with the additional information as member variables.
When you create a Thread object, you must supply a delegate that points to a method without parameters. This causes a problem if you need to pass information to the thread. The easiest solution is to wrap the threaded code and the required information into a single class.
Consider the example where you want to write a large amount of information to a file in the background. You can encapsulate this logic in the task class shown here:
Public Class FileSaver Private _FilePath As String Private _FileData() As Byte Public ReadOnly Property FilePath() As String Get Return _FilePath End Get End Property Public ReadOnly Property FileData() As Byte() Get Return _FileData End Get End Property Public Sub New(ByVal path As String, ByVal dataToWrite() As Byte) Me._FilePath = path Me._FileData = dataToWrite End Sub Public Sub Save() Dim fs As System.IO.FileStream Dim w As System.IO.BinaryWriter Try fs = New System.IO.FileStream(FilePath, IO.FileMode.OpenOrCreate) w = New System.IO.BinaryWriter(fs) w.Write(FileData) Finally ' This ensures that the file is closed, ' even if the thread is aborted. w.Close() End Try End Sub End Class
Now, to use this task class you create a FileSaver, set its properties accordingly, and then start the Save method using a new thread.
Public Module ThreadTest Public Sub Main() ' Create the task object. Dim Data() As Byte = {} Dim Saver As New FileSaver("myfile.bin", Data) ' Create the thread. Dim FileSaverThread As New Thread(AddressOf Saver.Save) FileSaverThread.Start() Console.WriteLine("Press Enter to exit...") Console.ReadLine() ' If the threads aren't yet finished, abort them. If (FileSaverThread.ThreadState And ThreadState.Running) = _ ThreadState.Running Then FileSaverThread.Abort() End If End Sub End Module
You want to execute a task on a separate thread, and you want to retrieve the data it produces.
Create a custom threaded class that incorporates a parameterless method, along with a method or property to retrieve the information. In addition, you might want to use an event to notify the caller.
When you create a Thread object, you must supply a delegate that points to a method without a return value. This limits your ability to retrieve information from the thread once its work is complete. The easiest solution is to wrap the threaded code and the return value into a single class. You can then add methods that allow the main application thread to retrieve the return value and progress information. You can also add a custom event that allows the threaded object to notify the main thread as soon as its task is finished.
For example, consider the weather lookup function provided in recipe 7.4. You could wrap this function into a threaded object that looks like this:
Public Class WeatherLookup ' The input information. Private _CityName As String ' The output information. Private _Temperature As Single ' The task progress informaton. Private _IsCompleted As Boolean = False ' An event used to notify when the task is complete. Public Event WeatherLookupCompleted(ByVal sender As Object, _ ByVal e As WeatherLookupCompletedEventArgs) Public ReadOnly Property CityName() As String Get Return _CityName End Get End Property Public Sub New(ByVal cityQuery As String) Me._CityName = cityQuery End Sub ' This method is executed on the new thread. Public Sub Lookup() ' This code would query the database for the requested info. ' Instead, we pause and return a random "temperature reading." Dim Rand As New Random() Delay(Rand.Next(5, 10)) _Temperature = Rand.Next(30, 100) _IsCompleted = True RaiseEvent WeatherLookupCompleted(Me, _ New WeatherLookupCompletedEventArgs(_CityName, _Temperature)) End Sub ' This method is called by the main thread to check the task status. Public ReadOnly Property IsCompleted() As Boolean Get Return _IsCompleted End Get End Property ' This method is called by the main thread to retrieve the task result. Public Function GetResult() As Single If _IsCompleted Then Return _Temperature Else Throw New InvalidOperationException("Not completed.") End If End Function ' (Delay function omitted.) End Class
Note |
In this example, the task object provides a simple IsCompleted flag that allows the main thread to check if the task is complete. Optionally, you could add finer-grained progress information, like a numeric PercentCompleted value that would indicate the amount of the task that has been completed. |
The WeatherLookupCompleted event uses a custom EventArgs object that contains information about the city name and retrieved temperature:
Public Class WeatherLookupCompletedEventArgs Inherits EventArgs Private _CityName As String Private _Temperature As Single Public ReadOnly Property CityName() As String Get Return _CityName End Get End Property Public ReadOnly Property Temperature() As Single Get Return _Temperature End Get End Property Public Sub New(ByVal cityName As String, ByVal temperature As Single) Me._CityName = cityName Me._Temperature = temperature End Sub End Class
This Console application creates a single task object, attaches an event handler, and starts the task on a separate thread:
Public Module ThreadTest Public Sub Main() ' Create the task object. Dim Lookup As New WeatherLookup("London") AddHandler Lookup.WeatherLookupCompleted, AddressOf LookupCompleted ' Create the thread. Dim LookupThread As New Thread(AddressOf Lookup.Lookup) LookupThread.Start() Console.WriteLine("Press Enter to exit...") Console.ReadLine() ' If the threads aren't yet finished, abort them. If (LookupThread.ThreadState And ThreadState.Running) = _ ThreadState.Running Then LookupThread.Abort() End If End Sub Private Sub LookupCompleted(ByVal sender As Object, _ ByVal e As WeatherLookupCompletedEventArgs) Console.WriteLine(e.CityName & " is: " & e.Temperature) End Sub End Module
Notice that the LookupCompleted event handler will actually execute on the lookup thread, not the main application thread. Thus, if you need to access a shared resource in this procedure, you need to use locking code.
With this approach, the main thread is still responsible for creating, tracking, and otherwise managing any threads it creates. It's possible, and often useful, to move this responsibility to the task class. One popular design pattern is to create a task class that acts as a thread wrapper, as shown in recipe 7.11.
You need to update a user interface element on a window from another thread.
Place your update logic in a separate subroutine, and use the Control.Invoke method to marshal this code to the user interface thread.
Windows controls are not thread-safe. That means that it isn't safe to update a user interface control from any thread other than the thread that created it. In fact, you might test code that ignores this restriction without experiencing any trouble, only to have the same code cause maddeningly elusive problems in a production environment.
This problem isn't restricted to code that executes in a custom-threaded object. It also applies to code that responds to a callback or event from a threaded object. That's because the callback or event-handling code will take place on the worker thread that raises the callback or event. Fortunately, you can solve this problem using the Invoke method, which is provided by all .NET controls. The Invoke method takes a MethodInvoker delegate that points to a method with no parameters or return value. The code in that method will be executed on the user interface thread.
' UpdateMethod() contains the code that modifies a Windows control. Dim UpdateDelegate As New MethodInvoker(AddressOf UpdateMethod) ' UpdateMethod() will now be invoked on the user interface thread. ControlToUpdate.Invoke(UpdateDelegate)
Of course, life often isn't this simple. The preceding approach works well if the update logic is hard-coded, but what if you need to update a control based on the contents of a variable? There's no way to use the MethodInvoker delegate to point to a procedure that accepts arguments, so you need to create a custom wrapper object.
One possibility is the custom ControlTextUpdater class shown in the following code. It references a single control and provides methods like AddText and ReplaceText. When called, these methods store the new text in a class member variable and marshal the call to the user interface thread using the Invoke method.
Public Class ControlTextUpdater ' The reference is retained as a generic control, ' allowing this helper class to be reused in other scenarios. Private _ControlToUpdate As Control Public ReadOnly Property ControlToUpdate() As Control Get Return _ControlToUpdate End Get End Property Public Sub New(ByVal controlToUpdate As Control) Me._ControlToUpdate = controlToUpdate End Sub ' Stores the text to add. Private _NewText As String Public Sub AddText(ByVal newText As String) SyncLock Me Me._NewText = newText ControlToUpdate.Invoke(New MethodInvoker( _ AddressOf ThreadSafeAddText)) End SyncLock End Sub Private Sub ThreadSafeAddText() Me.ControlToUpdate.Text &= _NewText End Sub Public Sub ReplaceText(ByVal newText As String) SyncLock Me Me._NewText = newText ControlToUpdate.Invoke(New MethodInvoker( _ AddressOf ThreadSafeReplaceText)) End SyncLock End Sub Private Sub ThreadSafeReplaceText() Me.ControlToUpdate.Text = _NewText End Sub End Class
As a precaution, the class locks itself just before updating the text. Otherwise, an update could be corrupted if a caller submits new text before the previous text has been applied.
To demonstrate how you would use a class like this in a Windows application, it helps to consider a more sophisticated example. In this case, we'll build on the weather lookup example described in recipe 7.8.
The application (shown in Figure 7-1) provides a single form that allows a user to submit multiple task requests at a time. The requests are processed on separate threads, and the results are written to a textbox in a thread-safe manner.
Figure 7-1: A front-end to a multithreaded task processor
The full code is available with the download for this chapter, although we'll examine the key elements here. First of all, the weather lookup code is modified slightly so that every weather lookup task has an automatically associated GUID. This ensures that the task can be uniquely identified.
Public Class WeatherLookup ' A unique identifier for this task. Private _Guid = Guid.NewGuid() Public ReadOnly Property Guid() As Guid Get Return _Guid End Get End Property ' (Other properties and methods omitted.) End Class
This GUID is also added to the WeatherLookupCompletedEventArgs, so the client knows what task has been completed when the event fires.
The form stores a hashtable collection that contains all the threads that are currently in progress.
Private TaskThreads As New Hashtable()
This collection allows the application to determine how many tasks are in progress at all times. A timer runs continuously, updating a panel in the status bar with this information:
Private Sub tmrRefreshStatus_Tick(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles tmrRefreshStatus.Tick pnlStatus.Text = TaskThreads.Count.ToString() & _ " task(s) in progress." End Sub
The use of a hashtable collection also allows the application to abort all tasks when it exits:
Private Sub Form1_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing ' Abort any in-progress threads. Dim Item As DictionaryEntry For Each Item In TaskThreads Dim TaskThread As Thread = Item.Value If (TaskThread.ThreadState And ThreadState.Running) = _ ThreadState.Running Then TaskThread.Abort() End If Next End Sub
When the Lookup button is clicked, a new task object and thread are created. The thread is stored in the TaskThreads collection, indexed by its GUID. The task object is not, although it could be.
Private Sub cmdLookup_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdLookup.Click ' Create the task object. Dim Lookup As New WeatherLookup(txtCity.Text) AddHandler Lookup.WeatherLookupCompleted, AddressOf LookupCompleted ' Create the thread. Dim LookupThread As New Thread(AddressOf Lookup.Lookup) LookupThread.Start() TaskThreads.Add(Lookup.Guid.ToString(), LookupThread) End Sub
When the lookup is completed, a WeatherLookupCompleted event fires. At this point, a new Updater is created to safely add the retrieved text to the textbox. Also, the corresponding thread is removed from the TaskThreads collection. Notice that synchronization code is required for this step because the TaskThreads collection might be accessed by another thread (for example, by the main thread if the timer fires or the user starts a new lookup).
Private Sub LookupCompleted(ByVal sender As Object, _ ByVal e As WeatherLookupCompletedEventArgs) ' Create the update object. Dim Updater As New ControlTextUpdater(txtResults) ' Perform a thread-safe update. Updater.AddText(e.CityName & " is: " & e.Temperature.ToString() & _ vbNewLine) ' Remove the task object. SyncLock TaskThreads TaskThreads.Remove(e.TaskGuid.ToString()) End SyncLock End Sub
Caution |
Allowing the user to create threads without any limit is a recipe for disaster. If the user makes use of this ability to generate dozens of threads, the entire system will run slower. The solution is to check the number of queued tasks and disable the Lookup button when it reaches a certain threshold. |
You want to stop a thread, but allow it to clean up the current task and end on its own terms.
Create a Boolean StopRequested flag that the threaded code can poll periodically.
When you use the Abort method to stop a thread, a ThreadAbortException is raised to the code that is currently running. The thread can handle this exception with code in a Catch or Finally block, but the exception cannot be suppressed. In other words, even if the exception is handled, it will be rethrown when the code in the Catch and Finally blocks finishes, until the thread finally terminates.
The Abort method isn't always appropriate. A less disruptive choice is to create a more considerate thread that checks for stop requests. For example, shown here is a custom-threaded object that loops for ten minutes, or until a stop request is received.
Public Class Worker Private _StopRequested As Boolean = False Public Sub RequestStop() _StopRequested = True End Sub Public Sub DoWork() Dim StartTime As DateTime = DateTime.Now Do If _StopRequested Then Exit Do End If Loop Until DateTime.Now.Subtract(StartTime).TotalMinutes > 9 End Sub End Class
Here is a Console application you can use to test the Worker class. Notice that three steps are taken:
Public Module ThreadTest Public Sub Main() ' Create the task object. Dim Worker As New Worker() ' Create the thread. Dim WorkerThread As New Thread(AddressOf Worker.DoWork) WorkerThread.Start() Console.WriteLine("Press Enter to request a stop...") Console.ReadLine() Worker.RequestStop() ' Wait for the thread to stop (or timeout after 30 seconds). WorkerThread.Join(TimeSpan.FromSeconds(30)) If WorkerThread.ThreadState = ThreadState.Running Then Console.WriteLine("An abort was required.") WorkerThread.Abort() End If Console.WriteLine("Thread is stopped.") End Sub End Module
Starting and stopping a thread is the most common type of interaction required between the main thread and worker thread of your application. However, you could add additional methods to allow you to send other instructions to your threaded code.
You want to remove the thread management code from your main thread and allow the task objects to manage their threads transparently.
Create a thread wrapper class that stores a reference to the thread and encapsulates the task-specific logic.
One common design pattern with multithreading is to create a thread wrapper. This wrapper provides the typical methods you would expect in a Thread, like Start and Stop, along with all the task-specific code, and provides properties for required input values and calculated values.
Here's an abstract base class that defines the basic structure of a well-behaved thread wrapper:
Public MustInherit Class ThreadWrapperBase ' This is the thread where the task is carried out. Public ReadOnly Thread As System.Threading.Thread Public Sub New() ' Create the thread. Me.Thread = New System.Threading.Thread(AddressOf Me.StartTask) End Sub ' Start the task on the worker thread. Public Overridable Sub Start() Me.Thread.Start() End Sub ' Stop the task by aborting the thread. ' You can override this method to use polite stop requests instead. Public Overridable Sub [Stop]() Me.Thread.Abort() End Sub ' Tracks the status of the task. Private _IsCompleted As Boolean Public ReadOnly Property IsCompleted() As Boolean Get Return _IsCompleted End Get End Property Private Sub StartTask() _IsCompleted = False DoTask() _IsCompleted = True End Sub ' This method contains the code that actually performs the task. Protected MustOverride Sub DoTask() End Class
And here's a very basic task class that derives from it:
Public Class Worker Inherits ThreadWrapperBase ' (You can add properties here for input values and output values.) ' (You can also define a WorkerCompleted event.) Protected Overrides Sub DoTask() Dim StartTime As DateTime = DateTime.Now Do ' Do nothing. Loop Until DateTime.Now.Subtract(StartTime).TotalMinutes > 4 End Sub End Class
The thread wrapper improves encapsulation and simplifies programming from the application's point of view, because it only has to track the Worker object (rather than the Worker object and the Thread object). Here's the code you would use to test the classes we've created:
Public Module ThreadTest Public Sub Main() ' Create the task object. Dim Worker As New Worker() ' Start the task on its internal thread. Worker.Start() Console.WriteLine("Press Enter to exit...") Console.ReadLine() ' Abort the task if needed. If Not Worker.IsCompleted Then Console.WriteLine("Thread is still running.") Worker.Stop() Console.WriteLine("Thread is aborted.") End If End Sub End Module
You need a dedicated thread that will process queue tasks and process them continuously.
Create a threaded class that stores tasks in a queue and monitors the queue continuously.
A common pattern in distributed application design is a task processor: a dedicated thread that performs requested tasks on a first-in-first-out basis. This thread lives for the life of the application and uses an internal collection (typically a queue) to store requested tasks until it has a chance to process them.
Creating a task processor is fairly easy. However, to do it properly, you need to leverage the stop request pattern and thread wrapper pattern shown in recipes 7.10 and 7.11, respectively.
The first step is to define a class that encapsulates task data. For this example, we allow any task to contain a block of binary data (perhaps containing an image that needs to be processed or a document that needs to be encrypted), and a GUID to uniquely identify the task.
Public Class Task Private _SomeData() As Byte Private _Guid As Guid = Guid.NewGuid() Public Property SomeData() As Byte() Get Return _SomeData End Get Set(ByVal Value() As Byte) _SomeData = Value End Set End Property Public ReadOnly Property Guid() As Guid Get Return _Guid End Get End Property End Class
The TaskProcessor class derives from ThreadWrapperBase (shown in recipe 7.11). It adds a SubmitTask method that allows new tasks to be entered into the queue and a GetIncompleteTaskCount method that allows you check the number of outstanding tasks. TaskProcessor also overrides the DoTask method with the worker code. DoTask runs continuously in a loop (provided a stop has not been requested), dequeuing items one at a time and then processing them. If no items are found, the thread is paused for 5 seconds. In addition, the Stop method is overridden so that it attempts to stop the thread cleanly with the _StopRequested flag before using an Abort.
Public Class TaskProcessor Inherits ThreadWrapperBase Private _Tasks As New Queue() Private _StopRequested As Boolean Public Sub SubmitTask(ByVal task As Task) SyncLock _Tasks _Tasks.Enqueue(task) End SyncLock End Sub Public Function GetIncompleteTaskCount() As Integer Return _Tasks.Count End Function Protected Overrides Sub DoTask() Do If _Tasks.Count > 0 Then SyncLock _Tasks ' Retrieve the next task (in FIFO order). Dim Task As Task = CType(_Tasks.Dequeue(), Task) End SyncLock ' (Process the task here.) Delay(10) Else ' No tasks are here. Suspend processing for 5 seconds. Thread.Sleep(TimeSpan.FromSeconds(5)) End If Loop Until _StopRequested End Sub Public Overrides Sub [Stop]() ' Request a polite stop for up to 10 seconds. _StopRequested = True Me.Thread.Join(TimeSpan.FromSeconds(10)) ' Call the base class implementation, ' which aborts the thread if necessary. MyBase.Stop() End Sub ' (Delay function omitted.) End Class
The following Console application demonstrates the task processor in action. It creates and submits three tasks, and it aborts the processor when the user presses the Enter key.
Public Module ThreadTest Public Sub Main() ' Create the task processor. Dim TaskProcessor As New TaskProcessor() ' Start the processor. TaskProcessor.Start() ' Assign three tasks. TaskProcessor.SubmitTask(New Task()) TaskProcessor.SubmitTask(New Task()) TaskProcessor.SubmitTask(New Task()) Console.WriteLine(TaskProcessor.GetIncompleteTaskCount().ToString & _ " tasks underway.") Console.WriteLine("Press Enter to stop...") Console.ReadLine() Console.WriteLine(TaskProcessor.GetIncompleteTaskCount().ToString & _ " tasks still underway.") TaskProcessor.Stop() End Sub End Module
You need to create an unbounded number of threads without degrading performance.
Use the ThreadPool class to map a large number of tasks to a fixed number of reusable threads.
The System.Threading.ThreadPool class allows you to execute code using a pool of threads provided by the common language runtime. Using this pool simplifies your code, and it can improve performance, particularly if you use a large number of short-lived threads. The ThreadPool class avoids the overhead of continually creating and destroying threads by using a pool of reusable threads. You use the ThreadPool class to queue a task, and the task is processed on the first available thread. The ThreadPool class is capped at 25 threads per CPU, which prevents the type of performance degradation that can occur when you create more threads than the operating system can efficiently schedule at one time.
Scheduling a task with ThreadPool is easy. You must simply meet two requirements. First of all, the method that you want to queue must match the signature of the WaitCallback delegate, which means they must accept a single state object:
Public Sub WaitCallback(ByVal state As Object) ' Code omitted. End Sub
The state object allows you to submit information about the task to the asynchronous method. However, if you are wrapping your task into a custom task object, you won't need to use this information, because all the state details will be provided in the task class itself.
Once you have a method that matches the required signature, you can pass it to the shared ThreadPool.QueueUserWorkItem method. (You can also supply the optional state object.) The QueueUserWorkItem method schedules the task for asynchronous execution. Assuming a thread is available, it will start processing in a matter of seconds.
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf WaitCallback), _ Nothing)
Here's a simple Worker class that can be used with ThreadPool:
Public Class Worker Private _IsCompleted As Boolean Public ReadOnly Property IsCompleted() As Boolean Get Return _IsCompleted End Get End Property Public Sub DoWork(ByVal state As Object) _IsCompleted = False Delay(10) _IsCompleted = True End Sub ' (Delay function omitted.) End Class
And here's a Console application that queues two Worker items for asynchronous execution and then waits for them to complete:
Public Module ThreadTest Public Sub Main() Dim WorkerThreads, CompletionPortThreads As Integer ThreadPool.GetMaxThreads(WorkerThreads, CompletionPortThreads) Console.WriteLine("This thread pool provides " & _ WorkerThreads.ToString() & " threads.") ThreadPool.GetAvailableThreads(WorkerThreads, CompletionPortThreads) Console.WriteLine(WorkerThreads.ToString() & _ " threads are currently available.") ' Create and queue two task objects. Dim WorkerA As New Worker() ThreadPool.QueueUserWorkItem( _ New WaitCallback(AddressOf WorkerA.DoWork), Nothing) Dim WorkerB As New Worker() ThreadPool.QueueUserWorkItem( _ New WaitCallback(AddressOf WorkerB.DoWork), Nothing) ThreadPool.GetAvailableThreads(WorkerThreads, CompletionPortThreads) Console.WriteLine(WorkerThreads.ToString() & _ " threads are currently available.") Do Loop Until WorkerA.IsCompleted And WorkerB.IsCompleted Console.WriteLine("All queued items completed.") ThreadPool.GetAvailableThreads(WorkerThreads, CompletionPortThreads) Console.WriteLine(WorkerThreads.ToString() & _ " threads are currently available.") Console.ReadLine() End Sub End Module
The display output for this application is as follows:
This thread pool provides 25 threads. 25 threads are currently available. 23 threads are currently available. All queued items completed. 25 threads are currently available.
Unfortunately, using .NET ThreadPool class isn't quite as powerful as designing your own thread pool. Notably, there is no way to cancel tasks once they are submitted or specify a priority so that some tasks are performed in a different order than they are received. It's also impossible to retrieve any information about ongoing tasks from ThreadPool—you can only inspect the number of available threads.
Note |
The ThreadPool class is an application-wide resource. That means that if you use the ThreadPool class to perform multiple different tasks, all your worker items will be competing for the same set of 25 threads. |
Introduction