Home > Articles > Programming > Windows Programming

Lightweight Threading with Thread Pools in Visual Basic .NET

  • Print
  • + Share This
A lot of excitement has been generated over the addition of multithreading capability to Visual Basic .NET. Get an introduction to lightweight threading in this article from Paul Kimmel, including example listings and sensible explanations.
This article is excerpted from Chapter 14, "Multithreaded Applications," of Visual Basic .NET Unleashed, by Paul Kimmel (Sams Publishing; ISBN 067232234x).
This chapter is from the book

This chapter is from the book

Visiting the Microsoft campus in Redmond, Washington in August, I asked a couple of developers what the difference is between using the thread class and using the thread pool. The answer I got was that there is no real difference. The thread pool is easier because it manages thread objects for you; when you create a thread object, you have to manage it yourself.

Using threads in the ThreadPool was referred to as "lightweight" threading, and creating an instance of the Thread class was referred to as "heavyweight" threading. The adjectives did not refer to their capability but rather to ease of use. The thread pool is easier to use, but when using the thread pool, you are multithreading just as assuredly as you are when creating instances of the Thread class. One developer said something to the effect of "Why wouldn't you always use the thread pool?"

In effect, identical end results can be achieved with lightweight threading or heavyweight threading. It's easy to use the thread pool, and a little harder to use the Thread class.

What Is the Thread Pool?

The thread pool is a class defined in the System.Threading namespace. The class is ThreadPool. What the ThreadPool class does is manage a few threads that are available for you to request work. If the pool has available threads, the work is completed on an available thread. If no thread is available in the pool, the thread pool creates another task or may wait for a thread to become available. For the most part, you do not care exactly how it proceeds.

Very simply, the thread pool uses an available thread or creates a new one, manages starting the task on the thread, and cleans up. The thread pool is a thread manager. A consequence is that if you use the thread pool, you do not need to create and keep track of individual thread objects, but you get the same benefit as if you had.

How Does the Thread Pool Work?

The thread pool works in much the same manner as creating and using an instance of the Thread class. You have a thread and you give it work by passing the thread a delegate. In the case of the thread pool, you give the pool a delegate and the pool manager assigns the work represented by the delegate to a thread. The result is the same.

Using the Thread Pool

You are familiar with keeping track of the time in a Windows application, so we will start there. (When you have the basics down, we will progress to more interesting tasks.)

There are three things we will need to use the thread pool in a Windows Form to implement a clock. We will need to define a procedure that interacts with the Windows Form on the same thread as the form. We will need to define a procedure that represents work occurring on a separate thread than the form, and we will need to request that the thread pool perform the work. Listing 14.2 demonstrates how straightforward this is.

Listing 14.2 Implementing a clock on a separate thread.

1: Imports System.Threading
3: Public Class Form1
4:  Inherits System.Windows.Forms.Form
6: [ Windows Form Designer generated code ]
8:  Private Sub UpdateTime()
9:   SyncLock Me.Name
10:    Text = Now
11:   End SyncLock
12:  End Sub
14:  Private Sub TrackTime(ByVal State As Object)
16:   While (True)
17:    Try
18:     'Invoke(New MethodInvoker(AddressOf UpdateTime))
19:    Invoke(CType(AddressOf UpdateTime, MethodInvoker))
20:    Catch
22:    End Try
23:    Thread.CurrentThread.Sleep(500)
24:   End While
26:  End Sub
28:  Private Sub Form1_Load(ByVal sender As System.Object, _
29:   ByVal e As System.EventArgs) Handles MyBase.Load
31:   ThreadPool.QueueUserWorkItem(AddressOf TrackTime)
33:  End Sub
34: End Class

UpdateTime on lines 8 through 12 updates the form's caption—Text property—to display the current time. (We have dispensed with the StatusBar because it isn't relevant to the discussion.) We use SyncLock and End SyncLock to block any other thread from trying to update the text property, but what makes the code safe is that UpdateTime occurs on the same thread that the form is on. (We will inspect this hypothesis in a minute.)

TrackTime has the signature of a WaitCallback delegate. WaitCallback is initialized with a subroutine that takes a single Object argument. Line 16 begins an infinite loop. We know from experience, of course, that an infinite loop in our main thread would spell death in the form of unresponsiveness to our application. Because TrackTime runs on its own thread, infinite-loop death does not occur. Lines 18 and 19 are effectively identical. Lines 18 and 19 use the Invoke method (which all controls have), which allows you to invoke a process. Calling Invoke bumps the work over to the thread that the control is on. On line 18 we are indicating that we want to invoke the UpdateMethod on the form's thread. Implicit in the call on lines 18 and 19 is the Me object reference.

Finally, line 31 calls the shared method ThreadPool.QueueUserWorkItem passing a delegate returned by the AddressOf statement as the work item. Line 31 will place TrackTime on its own thread. Figures 14.1 through 14.3 show the threads running and the changing of contexts as the code runs. A brief explanation follows each figure.

Figure 14.1. Form1.TrackTime shown on a separate thread, thread ID 2460.

Figure 14.1 shows the debugger stopped on line 63 on the statement Thread.CurrentThread.Sleep(500). From the Threads window—which you can open by choosing Debug, Windows, Threads in the Visual Studio .NET IDE—you can see that the TrackTime method is running on thread 2460. We use the Step Into shortcut until the debugger reaches line 57 in the TrackTime method. We use Debug, Step Into twice more until the debugger reaches line 60, which contains an Invoke method call.

Figure 14.2. Form1.UpdateTime shown on the same thread as the Form itself, thread 2324.

From Figure 14.2, you can see that the Invoke method caused the debugger to switch threads. UpdateTime is running on thread 2324. If we continue stepping to the end of UpdateTime, we see that the thread switches back to 2460 after the debugger returns from UpdateTime (see Figure 14.3).

Figure 14.3. Form1.TrackTime shown after returning from UpdateTime and back on thread 2460.

But how do we know we are on the same thread as the form? There are two ways we can determine that UpdateTime is on the same thread as the form. When the Form.Load event occurs, we can use the QuickWatch window, accessed by pressing Shift+F9 and invoking the AppDomain.GetCurrentThreadID shared method. This method will indicate the form's thread and we can visually compare it to the thread ID in the Threads window when UpdateTime is processing. The second way we can know if the UpdateTime is on the form's thread is by calling Control.InvokeRequired.

Each control implements InvokeRequired. Calling InvokeRequired compares the control's thread with the thread on which the InvokeRequired method was called. If the threads are identical, InvokeRequired returns False.


There is a problem with the code example in Listing 14.2. What if the form is shutting down or disposed of and the code calls the form's Invoke method on line 18? Although the help indicates that Invoke is safe to call from any thread, you still can't call a method on an object that has been disposed of. You could write to check to see if the form is Disposing, but if the form is already disposed of, this will fail.

You could check the IsDisposed property. This property will return True if the form is disposed of, but the garbage collector has not cleaned up the memory yet. However, if the GC has cleaned up the form, you will still get an exception.

You could use a flag in the form that indicates that the form is being closed, but the Invoke method could be called after the flag is checked.


For this example I would make one of three decisions based on the importance of the task. One choice would be to consider the task simplistic enough that a silent exception handler around the Invoke call would catch calls after the form had been destroyed.

 Invoke(CType(AddressOf UpdateTime, MethodInvoker))
End Try

Where the form has been disposed of, this silent exception handler would provide blanket protection. Because there is nothing to corrupt here, this is a reasonable solution. I am not a big fan of silent exceptions but do use them on rare occasions. The relatively low importance of keeping time might warrant such an approach.

A second choice would be to create the thread myself and keep track of the thread, shutting down and disposing of the thread when the application shuts down. This solution is clean and demonstrates an instance when owning the thread helps.

A third choice would to consider the relatively low importance of the task and use a timer to get asynchronous background behavior. In a real-world application where the timer is simply providing a clock, this is the choice I would make.

Using a WaitHandle and Synchronizing Behavior

The WaitHandle class is a base class used to implement synchronization objects. AutoResetEvent, ManualResetEvent, and Mutex are subclassed from WaitHandle and define methods to block access to shared resources.

To demonstrate blocking and synchronization of shared resources, I will implement a class named Dice. Each Dice instance rolls on its own thread, but the total score of all of the dice cannot be obtained until all of the dice have finished rolling. WaitHandle objects are used in conjunction with the thread pool, so we will roll the dice using the threads in the pool.

Listing 14.3 implements Dice and DiceGraphic classes. The Dice class represents a single die and the DiceGraphic class supports painting the graphical view of one face of a die. Listing 14.3 contains the code that runs on a unique thread, contains the shared WaitHandle, and uses synchronization to determine when all dice have finished rolling. Listing 14.4 lists the form that contains the graphical representation of five dice. A synopsis of the code follows each listing.

Listing 14.3 Contains the threaded behavior, WaitHandle, and synchronized behavior.

1: Imports System.Threading
2: Imports System.Drawing
4: Public Class Dice
6:  Private FValue As Integer = 1
7:  Private Shared FRolling As Integer = 0
8:  Private FColor As Color
9:  Private FRect As Rectangle
10:  Public Shared Done As New AutoResetEvent(False)
12:  Public Shared ReadOnly Property IsRolling() As Boolean
13:   Get
14:    Return FRolling > 0
15:   End Get
16:  End Property
18:  Public Sub New()
19:   MyClass.New(New Rectangle(10, 10, 50, 50), Color.White)
20:  End Sub
22:  Public Sub New(ByVal Rect As Rectangle, ByVal color As Color)
23:   MyBase.New()
24:   FRect = Rect
25:   FColor = color
26:  End Sub
28:  Public ReadOnly Property Value() As Integer
29:   Get
30:    Return FValue
31:   End Get
32:  End Property
34:  Public Sub Roll(ByVal State As Object)
36:   Interlocked.Increment(FRolling)
37:   Try
38:    DoRoll(CType(State, Graphics))
39:   Finally
40:    If (Interlocked.Decrement(FRolling) = 0) Then
41:     Done.Set()
42:    End If
43:   End Try
45:  End Sub
47:  Public Sub Draw(ByVal Graphic As Graphics)
48:   DiceGraphic.Draw(Graphic, FValue, FRect, FColor)
49:  End Sub
51:  Private Sub DoRoll(ByVal Graphic As Graphics)
52:   Dim I As Integer = GetRandomNumber()
53:   While (I > 0)
54:    FValue = GetRandomDie()
55:    Draw(Graphic)
56:    Beep()
57:    I -= 1
58:    Thread.CurrentThread.Sleep(50)
59:   End While
60:  End Sub
62:  Private Shared Random As New Random()
64:  Private Shared Function GetRandomNumber() As Integer
65:   Return Random.Next(30, 50)
66:  End Function
68:  Protected Shared Function GetRandomDie() As Integer
69:   Return Random.Next(1, 7)
70:  End Function
71: End Class
73: Public Class DiceGraphic
75:  Public Shared Sub Draw(ByVal Graphic As Graphics, _
76:   ByVal Value As Integer, _
77:   ByVal Rect As Rectangle, ByVal Color As Color)
79:   Graphic.FillRectangle(New SolidBrush(Color), Rect)
80:   Graphic.DrawRectangle(Pens.Black, Rect)
81:   DrawDots(Graphic, GetRects(Value, Rect))
83:  End Sub
86:  Private Shared Function GetRects(ByVal Value As Integer, _
87:   ByVal Rect As Rectangle) As Rectangle()
89:   Dim One() As Rectangle = {GetRectangle(Rect, 1, 1)}
90:   Dim Two() As Rectangle = {GetRectangle(Rect, 0, 2), _
91:   GetRectangle(Rect, 2, 0)}
93:   Dim Three() As Rectangle = {GetRectangle(Rect, 0, 2), _
94:   GetRectangle(Rect, 1, 1), GetRectangle(Rect, 2, 0)}
96:   Dim Four() As Rectangle = {GetRectangle(Rect, 0, 0), _
97:    GetRectangle(Rect, 0, 2), GetRectangle(Rect, 2, 0), _
98:    GetRectangle(Rect, 2, 2)}
100:   Dim Five() As Rectangle = {GetRectangle(Rect, 0, 0), _
101:   GetRectangle(Rect, 1, 1), GetRectangle(Rect, 0, 2), _
102:   GetRectangle(Rect, 2, 0), GetRectangle(Rect, 2, 2)}
104:   Dim Six() As Rectangle = {GetRectangle(Rect, 0, 0), _
105:    GetRectangle(Rect, 0, 1), GetRectangle(Rect, 0, 2), _
106:    GetRectangle(Rect, 2, 0), GetRectangle(Rect, 2, 1), _
107:    GetRectangle(Rect, 2, 2)}
109:   Dim Rects As Rectangle()() = _
110:    {One, Two, Three, Four, Five, Six}
112:   Return Rects(Value - 1)
114:  End Function
116:  Protected Shared Function GetRectangle(ByVal Rect As Rectangle, _
117:   ByVal X As Integer, ByVal Y As Integer) As Rectangle
119:   Return New Rectangle(Rect.X + _
120:    (Rect.Width * X / 3), _
121:    Rect.Y + (Rect.Height * Y / 3), _
122:    GetDotSize(Rect).Width, GetDotSize(Rect).Height)
123:  End Function
126:  Protected Shared Function GetDotSize( _
127:   ByVal Rect As Rectangle) As Size
129:   Return New Size(Rect.Width / 3, Rect.Height / 3)
130:  End Function
132:  Private Shared Sub DrawDot(ByVal Graphic As Graphics, _
133:   ByVal Rect As Rectangle)
135:   Graphic.SmoothingMode = _
136:    Drawing.Drawing2D.SmoothingMode.AntiAlias
138:   Rect.Inflate(-3, -3)
139:   Graphic.FillEllipse(New SolidBrush(Color.Black), Rect)
141:  End Sub
143:  Private Shared Sub DrawDots(ByVal Graphic As Graphics, _
144:  ByVal Rects() As Rectangle)
146:   Dim I As Integer
147:   For I = 0 To Rects.Length - 1
148:    DrawDot(Graphic, Rects(I))
149:   Next
151:  End Sub
153: End Class

Listing 14.3 implements the Dice class as a class that rotates a random number of times through the values 1 through 6. During each roll (see lines 51 through 60), a random value for the dice is obtained, Beep is used to simulate the sound of rolling dice, and the die is drawn. The drawing of the die's face is managed by the DiceGraphic class using GDI+ (see Chapter 17, "Programming with GDI+," for more information on using the Graphics object).

Transitioning to the topic of our discussion, the rolling behavior is run on its own thread invoked by an external source. Lines 34 through 45 implement the rolling behavior. Line 36 calls the shared Interlocked.Increment(FRolling) method to perform an atomic increment of the shared FRolling field. Dice are rolling when FRolling > 0, as implemented by the shared IsRolling property of the Dice class. A resource protection block is used to ensure that the FRolling property is decremented. The rolling behavior is called on line 38. From the typecast on line 38—CType(State, Graphics))—it is apparent that we will be passing in the Graphics object each time we roll the dice, because GDI+ is stateless. The Graphics object represents the device context, or canvas, of the control we are painting on, and its stateless implementation simply means that we do not cache Graphics objects. The Finally block ensures that the FRolling field is decremented, again using an atomic shared method Interlocked.Decrement. The new value of FRolling is evaluated. If FRolling = 0 after it has been decremented, all dice have stopped rolling and we can signal the WaitHandle that we are finished.

Done is instantiated on line 10 as an AutoResetEvent. AutoResetEvent is subclassed from WaitHandle, and it is created in an unsignaled state, represented by the False argument. Done is shared because one WaitHandle is shared by all instances of Dice. In summary, each Dice instance increments the shared FRolling field and decrements it when it is finished rolling. When FRolling is 0 again, we notify whoever is waiting that all dice are finished rolling. Listing 14.4 demonstrates a client that shows the dice (see Figure 14.4).

Figure 14.4. The threaded dice after they have been rolled on their own threads.

Listing 14.4 Each die rolls on its own thread, while waiting for all dice before scoring the roll.

1: Option Explicit On 
2: Option Strict On
4: Imports System.Threading
6: Public Class Form1
7:  Inherits System.Windows.Forms.Form
9: [ Windows Form Designer generated code ]
11:  Private FDice(4) As Dice
13:  Private Sub Form1_Load(ByVal sender As System.Object, _
14:   ByVal e As System.EventArgs) Handles MyBase.Load
16:   Dim I As Integer
17:   For I = 0 To FDice.Length - 1
18:    FDice(I) = New Dice(New Rectangle(54 * I, 10, 50, 50), _
19:     Color.Ivory)
20:   Next
21:  End Sub
23:  Private Sub RollDice()
24:   Dim I As Integer
25:   For I = 0 To FDice.Length() - 1
26:    ThreadPool.QueueUserWorkItem(AddressOf FDice(I).Roll, CreateGraphics)
27:   Next
29:   Dice.Done.WaitOne()
30:  End Sub
32:  Private Sub Score()
33:   Dim I, Sum As Integer
34:   For I = 0 To FDice.Length() - 1
35:    Sum += FDice(I).Value
36:   Next
38:   Text = String.Format("Scored: {0}", Sum)
39:  End Sub
41:  Private Sub Button1_Click(ByVal sender As System.Object, _
42:   ByVal e As System.EventArgs) Handles Button1.Click
44:   RollDice()
45:   Score()
47:  End Sub
49:  Private Sub Form1_Paint(ByVal sender As Object, _
50:   ByVal e As System.Windows.Forms.PaintEventArgs) _
51:   Handles MyBase.Paint
53:   Dim I As Integer
54:   For I = 0 To FDice.Length - 1
55:    FDice(I).Draw(CreateGraphics)
56:   Next
58:  End Sub
60: End Class 


Note: The threaded rolling behavior is cool, but it is worth noting that it took me about five times longer to write a threaded version of the rolling dice and get it to work correctly than simply rolling all dice on the same thread as the form.

Most of the code in Listing 14.4 is straightforward, so I won't itemize all of it. To review, the form is created. Five Dice are constructed in the form's Load event. The form's Paint event ensures that the dice are repainted if the form is repainted. (If the dice were user controls, they would receive their own paint message.) When the user clicks the button labeled Roll (refer to Figure 14.4), the RollDice and Score methods are called. The Score method simply sums the Value of each die. The interesting bit happens in the RollDice method.

The RollDice method on lines 23 through 30 iterates over each Dice in the FDice array declared on line 11. The Roll method of each Dice object is treated as the WaitCallback argument of the shared ThreadPool.QueueUserWorkItem method. Dice.Roll represents the work. The second argument is a Graphics object returned by the CreateGraphics factory method. After the loop exits, each dice is rolling on its own thread in the ThreadPool.

Resynchronizing occurs on line 29. The shared AutoResetEvent object is used to wait for all of the dice to stop rolling. Recall that the code does not call AutoResetEvent.Set until IsRolling is False, that is, until all dice have stopped rolling. By implementing the code this way, the message queue is filling up with input but not responding until AutoResetEvent.WaitOne (represented on line 29 by Done.WaitOne) returns.


The first time you roll the dice, there is a brief delay between when the first die begins rolling and each subsequent die. This reflects the time it takes for the thread pool to construct additional thread objects. Subsequent rolls appear to start almost concurrently.

If you try to close the form, for example, the application will wait until the dice have stopped rolling before responding to an application shutdown. If you try to roll a second time before an ongoing roll is over, the application will respond after WaitOne returns. You would not want to be using the Graphics object passed to each die if the form object were being destroyed. Finally, because each die paints itself, you get a smooth graphic result without repainting the entire form, which would result in flicker.


The ManualResetEvent is a WaitHandle that remains signaled until the Reset method is called, and remains unsignaled until the Set method is called.


Mutex is a synchronization primitive that provides synchronized access to a shared resource. If one thread acquires a mutex, subsequent threads are blocked until the first thread releases its mutex.

Synchronization with the Monitor Class

Synchronizing critical sections of your code is essential when you may have multiple threads accessing a shared section of your code. For general synchronization, you can use the SyncLock...End SyncLock construct.

The SyncLock...End SyncLock construct is implemented using the Monitor class. You cannot create an instance of Monitor; all of the methods are shared anyway. Invoking Monitor.Enter(object) and Monitor.Exit(object) is identical to using the SyncLock...End SyncLock construct.

Monitor also contains methods Pulse, PulseAll, TryEnter, and Wait. Pulse notifies a single object in the waiting queue of a state change in the locked object. PulseAll notifies all waiting threads of a state change, and Wait releases the lock and waits until it reacquires the lock. The TryEnter method attempts to acquire an exclusive lock on an object.

Listing 14.5 demonstrates how to use the Monitor class to switch back and forth between two threads interacting with the same object.

Listing 14.5 Using the Monitor class.

1: Option Explicit On 
2: Option Strict On
4: Imports System
5: Imports System.Threading
7: Class MonitorDemo
9:  Private Integers() As Integer
10:  Private MAX As Integer = 1000
12:  Private I, J As Integer
14:  Public Sub FillArray()
15:   Dim I As Integer
16:   ReDim Integers(MAX)
17:   Dim R As New Random()
19:   For I = 0 To Integers.Length - 1
20:    Integers(I) = Integers.Length - 1 - I
21:   Next
22:  End Sub
24:  Public Sub SortArray(ByVal State As Object)
25:   Monitor.Enter(Integers)
27:   For I = 0 To Integers.Length - 1
28:    For J = I + 1 To Integers.Length - 1
29:     If (Integers(I) > Integers(J)) Then
30:      Dim T As Integer = Integers(I)
31:      Integers(I) = Integers(J)
32:      Integers(J) = T
33:     End If
34:    Next
36:    Monitor.Wait(Integers)
37:    Console.Write("Sorted: ")
38:    Monitor.Pulse(Integers)
39:   Next
41:   Monitor.Exit(Integers)
42:  End Sub
44:  Public Sub PrintArray(ByVal State As Object)
45:   Static K As Integer = 0
47:   Monitor.Enter(Integers)
48:   Monitor.Pulse(Integers)
50:   While (Monitor.Wait(Integers, 1000))
52:    If (K <= I) Then
53:     Console.WriteLine(Integers(K))
54:     K += 1
55:    End If
57:    Monitor.Pulse(Integers)
58:   End While
60:   Monitor.Exit(Integers)
61:  End Sub
63:  Public Shared Sub Main()
65:   Dim Demo As New MonitorDemo()
66:   Demo.FillArray()
68:   ThreadPool.QueueUserWorkItem(AddressOf Demo.SortArray)
69:   ThreadPool.QueueUserWorkItem(AddressOf Demo.PrintArray)
71:   Console.ReadLine()
73:  End Sub
75: End Class

Listing 14.5 uses Monitor.Enter and Monitor.Exit on lines 25 and 41 and again on lines 47 and 60. We would get the same result if we used the SyncLock...End SyncLock construct.

The Main subroutine is the starting point for this console application. An instance of the MonitorDemo class is created on line 65 and an array is filled with a thousand integers in reverse order. The ThreadPool is used on lines 68 and 69 requesting work from the SortArray and PrintArray methods. SortArray sorts the array of integers and PrintArray prints the integers in the array.

After each complete pass through the inner loop of the bubble sort, Monitor.Wait is called on line 36, giving the PrintArray method a chance to print the ordered ith element. Line 57 calls Monitor.Pulse notifying the SortArray method that the state has changed and allowing SortArray to reacquire the lock. The Monitor.Wait call on line 50 blocks the loop until the PrintArray method can reacquire the lock on the Integers object or one thousand milliseconds have elapsed. In summary, the code sorts each ith element and then prints the newly sorted element at the ith position.


The CLR supports asynchronous processing, lightweight threading using ThreadPool, and heavyweight threading by constructing instances of the Thread class. You are not limited to an all-or-nothing approach when implementing asynchronous or threaded behavior.

Choose the Timer control or Application.Idle event or BeginInvoke and EndInvoke for lightweight asynchronous behavior in Windows Forms. Consider using ThreadPool for many everyday multithreading tasks, and pull out the big gun—the Thread class—if you need absolute control. Of course, when using the Thread class, you have to take complete ownership of the behavior of the thread, including creating, starting, and stopping the thread.

The CLR, and consequently Visual Basic .NET, support asynchronous and multithreaded behavior as well as a whole complement of synchronization and shared resource management by using the WaitHandle or Monitor classes. Consider all of the available resources for asynchronous and threaded behavior before selecting a particular implementation strategy.

  • + Share This
  • 🔖 Save To Your Account