Lightweight Threading with Thread Pools

Team-Fly    

 
Visual Basic .NET Unleashed
By Paul Kimmel
Table of Contents
Chapter 14.  Multithreaded Applications

Lightweight Threading with Thread Pools

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

UpdateTime on lines 8 through 12 updates the form's captionText propertyto 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.

graphics/14fig01.jpg

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

graphics/14fig03.jpg

Figure 14.1 shows the debugger stopped on line 63 on the statement Thread.CurrentThread.Sleep(500). From the Threads windowwhich you can open by choosing Debug, Windows, Threads in the Visual Studio .NET IDEyou 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.

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.2. Form1.UpdateTime shown on the same thread as the Form itself, thread 2324.

graphics/14fig02.jpg

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.

Problems

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.

Resolutions

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 .

 Try   Invoke(CType(AddressOf UpdateTime, MethodInvoker)) Catch 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 I 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 be 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  3:   4:  Public Class Dice  5:   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)  11:   12:  Public Shared ReadOnly Property IsRolling() As Boolean  13:  Get  14:  Return FRolling > 0  15:  End Get  16:  End Property  17:   18:  Public Sub New()  19:  MyClass.New(New Rectangle(10, 10, 50, 50), Color.White)  20:  End Sub  21:   22:  Public Sub New(ByVal Rect As Rectangle, ByVal color As Color)  23:  MyBase.New()  24:  FRect = Rect  25:  FColor = color  26:  End Sub  27:   28:  Public ReadOnly Property Value() As Integer  29:  Get  30:  Return FValue  31:  End Get  32:  End Property  33:   34:  Public Sub Roll(ByVal State As Object)  35:   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  44:   45:  End Sub  46:   47:  Public Sub Draw(ByVal Graphic As Graphics)  48:  DiceGraphic.Draw(Graphic, FValue, FRect, FColor)  49:  End Sub  50:   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  61:   62:  Private Shared Random As New Random()  63:   64:  Private Shared Function GetRandomNumber() As Integer  65:  Return Random.Next(30, 50)  66:  End Function  67:   68:  Protected Shared Function GetRandomDie() As Integer  69:  Return Random.Next(1, 7)  70:  End Function  71:  End Class  72:   73:  Public Class DiceGraphic  74:   75:  Public Shared Sub Draw(ByVal Graphic As Graphics, _  76:  ByVal Value As Integer, _  77:  ByVal Rect As Rectangle, ByVal Color As Color)  78:   79:  Graphic.FillRectangle(New SolidBrush(Color), Rect)  80:  Graphic.DrawRectangle(Pens.Black, Rect)  81:  DrawDots(Graphic, GetRects(Value, Rect))  82:   83:  End Sub  84:   85:   86:  Private Shared Function GetRects(ByVal Value As Integer, _  87:  ByVal Rect As Rectangle) As Rectangle()  88:   89:  Dim One() As Rectangle = {GetRectangle(Rect, 1, 1)}  90:  Dim Two() As Rectangle = {GetRectangle(Rect, 0, 2), _  91:  GetRectangle(Rect, 2, 0)}  92:   93:  Dim Three() As Rectangle = {GetRectangle(Rect, 0, 2), _  94:  GetRectangle(Rect, 1, 1), GetRectangle(Rect, 2, 0)}  95:   96:  Dim Four() As Rectangle = {GetRectangle(Rect, 0, 0), _  97:  GetRectangle(Rect, 0, 2), GetRectangle(Rect, 2, 0), _  98:  GetRectangle(Rect, 2, 2)}  99:   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)}  103:   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)}  108:   109:  Dim Rects As Rectangle()() = _  110:  {One, Two, Three, Four, Five, Six}  111:   112:  Return Rects(Value - 1)  113:   114:  End Function  115:   116:  Protected Shared Function GetRectangle(ByVal Rect As Rectangle, _  117:  ByVal X As Integer, ByVal Y As Integer) As Rectangle  118:   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  124:   125:   126:  Protected Shared Function GetDotSize(_  127:  ByVal Rect As Rectangle) As Size  128:   129:  Return New Size(Rect.Width / 3, Rect.Height / 3)  130:  End Function  131:   132:  Private Shared Sub DrawDot(ByVal Graphic As Graphics, _  133:  ByVal Rect As Rectangle)  134:   135:  Graphic.SmoothingMode = _  136:  Drawing.Drawing2D.SmoothingMode.AntiAlias  137:   138:  Rect.Inflate(-3, -3)  139:  Graphic.FillEllipse(New SolidBrush(Color.Black), Rect)  140:   141:  End Sub  142:   143:  Private Shared Sub DrawDots(ByVal Graphic As Graphics, _  144:  ByVal Rects() As Rectangle)  145:   146:  Dim I As Integer  147:  For I = 0 To Rects.Length - 1  148:  DrawDot(Graphic, Rects(I))  149:  Next  150:   151:  End Sub  152:   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 38CType(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.

graphics/14fig04.jpg

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  3:   4:  Imports System.Threading  5:   6:  Public Class Form1  7:  Inherits System.Windows.Forms.Form  8:   9:  [ Windows Form Designer generated code ]  10:   11:  Private FDice(4) As Dice  12:   13:  Private Sub Form1_Load(ByVal sender As System.Object, _  14:  ByVal e As System.EventArgs) Handles MyBase.Load  15:   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  22:   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  28:   29:  Dice.Done.WaitOne()  30:  End Sub  31:   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  37:   38:  Text = String.Format("Scored: {0} ", Sum)  39:  End Sub  40:   41:  Private Sub Button1_Click(ByVal sender As System.Object, _  42:  ByVal e As System.EventArgs) Handles Button1.Click  43:   44:  RollDice()  45:  Score()  46:   47:  End Sub  48:   49:  Private Sub Form1_Paint(ByVal sender As Object, _  50:  ByVal e As System.Windows.Forms.PaintEventArgs) _  51:  Handles MyBase.Paint  52:   53:  Dim I As Integer  54:  For I = 0 To FDice.Length - 1  55:  FDice(I).Draw(CreateGraphics)  56:  Next  57:   58:  End Sub  59:   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.

Note

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.

ManualResetEvent

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

Mutex

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  3:   4:  Imports System  5:  Imports System.Threading  6:   7:  Class MonitorDemo  8:   9:  Private Integers() As Integer  10:  Private MAX As Integer = 1000  11:   12:  Private I, J As Integer  13:   14:  Public Sub FillArray()  15:  Dim I As Integer  16:  ReDim Integers(MAX)  17:  Dim R As New Random()  18:   19:  For I = 0 To Integers.Length - 1  20:  Integers(I) = Integers.Length - 1 - I  21:  Next  22:  End Sub  23:   24:  Public Sub SortArray(ByVal State As Object)  25:  Monitor.Enter(Integers)  26:   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  35:   36:  Monitor.Wait(Integers)  37:  Console.Write("Sorted: ")  38:  Monitor.Pulse(Integers)  39:  Next  40:   41:  Monitor.Exit(Integers)  42:  End Sub  43:   44:  Public Sub PrintArray(ByVal State As Object)  45:  Static K As Integer = 0  46:   47:  Monitor.Enter(Integers)  48:  Monitor.Pulse(Integers)  49:   50:  While (Monitor.Wait(Integers, 1000))  51:   52:  If (K <= I) Then  53:  Console.WriteLine(Integers(K))  54:  K += 1  55:  End If  56:   57:  Monitor.Pulse(Integers)  58:  End While  59:   60:  Monitor.Exit(Integers)  61:  End Sub  62:   63:  Public Shared Sub Main()  64:   65:  Dim Demo As New MonitorDemo()  66:  Demo.FillArray()  67:   68:  ThreadPool.QueueUserWorkItem(AddressOf Demo.SortArray)  69:  ThreadPool.QueueUserWorkItem(AddressOf Demo.PrintArray)  70:   71:  Console.ReadLine()  72:   73:  End Sub  74:   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 i th 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 i th element and then prints the newly sorted element at the i th position.


Team-Fly    
Top
 


Visual BasicR. NET Unleashed
Visual BasicR. NET Unleashed
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 222

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