Home > Articles > Programming > Windows Programming

Lightweight Threading with Thread Pools in Visual Basic .NET

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
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 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.

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 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
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 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
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

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 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.

Summary

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.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020