In that case, if yourcode produces multiple threads, the program will run faster because each thread can run on a separateprocessor.. For example, you can perform long calcula-tions or d
Trang 1Most computers today have only a single central processing unit (CPU) that executes instructions.That means they can only do one thing at a time Modern CPUs are pretty fast and can executemany millions or even billions of instructions per second Most applications don’t require thatmuch speed, so the computer splits the CPU’s time to let several different applications take turnsrunning The CPU is so fast that the switching takes place quickly enough to give the illusion thatthe computer is running more than one program at the same time While the CPU can really onlyexecute one instruction at a time, it appears as if the Windows operating system is running awhole bunch of applications simultaneously
Each of the programs running on the computer is a separate process Within a process, you can ate multiple threads of execution that can also run simultaneously Every program has at least one
cre-process and thread, but some create more These threads are more closely related than the cesses representing different applications, but the idea is similar Like processes, the threads taketurns running on the CPU so they give the illusion of running at the same time
pro-Typically, threads must communicate and work together to provide whatever application it is thatthey build as a group To avoid confusion, the operating system imposes strict rules on howthreads can communicate safely It’s easier for different threads to communicate than it is for dif-ferent processes because they are more closely related, but there are still rules to follow
For example, you could build a single application that monitors a stock quote Web site to trackyour favorite stocks Separate threads could monitor each of several stocks and display their cur-rent prices on separate graphs within the same application Each thread works separately, but theyall interact with the main program’s form to display their data
This chapter describes some useful threading scenarios It explains how to build applications thatuse threading to make the user interface more responsive, to perform tasks in the backgroundwhile the user does other things, and to divide complex tasks into pieces
Trang 2Threading Pros and Cons
The technology used to build computer chips is reaching its physical limits, so the steady increases inCPU speed that developers have come to expect over the last few decades is starting to slow down.Because they are already using the fastest chips available, computer manufacturers cannot rely on evenfaster chips to improve performance Instead, many computers now use multiple CPUs to improve per-formance It is quite common these days to find computers using two CPUs, and it seems likely that thetrend toward multiple CPUs will continue Systems with up to eight processors will probably be avail-able by the time you read this book
The exact definition of these systems is sometimes a little fuzzy Some “multi-core” systems have rate CPU chips, while others have multiple processors on a single chip Some architectures have a cen- tral processor that controls several secondary processors For the purposes of this chapter, all that
sepa-matters is that the computer has more than one processor of some kind that can execute threads.
If you want to take full advantage of multi-CPU systems, you should know a bit about multi-threadedapplications The following sections describe some of the advantages and disadvantages of multi-threaded programming
Advantages
Often, a multiple CPU system can execute different threads on different processors In that case, if yourcode produces multiple threads, the program will run faster because each thread can run on a separateprocessor The program probably won’t run twice as fast, because there is some overhead in coordinat-ing the threads, but it will probably run significantly faster than it would on a single CPU system
Some multi-processor systems can automatically parallelize instructions and run pieces of the tion in separate threads For example, suppose a program must perform some calculations on some old values, read some new values from a file, and then combine the old and new values The computer could start a thread running on one CPU to read values from the file, while another thread on a separate CPU performs the initial calculations Then, the threads would need to synchronize so that they can combine the new and old values Though the entire sequence cannot be split across two processors, some of the steps can be.
applica-Even if the computer has a single processor, writing multiple threads can often make the applicationappear faster and more responsive to the user For example, suppose a program must perform a verylong series of calculations In a single-threaded application, the program begins its calculation and can-not respond until it finishes A well-written application provides feedback so that the user knows theprogram is still working A poorly written application may appear stuck and might not even refresh itsuser interface If the user covers and exposes the program, its form may not even redraw
To keep the application responsive, you can start the long calculation on a separate thread The systemautomatically switches control between this calculation thread and the application’s user interface
thread (UI thread) so that the user interface can update itself.
Trang 3The user interface can also respond to user actions such as button clicks In particular, the user may beable to click a button to cancel the long calculation.
A program can also control a thread’s priority The system gives precedence to higher-priority threads.You can make high-priority threads run important calculations, while lower-priority threads performless-important tasks when the high-priority threads are idle For example, you can perform long calcula-tions or downloads on a lower-priority thread, so the user interface takes precedence Because the userinterface spends most of its time waiting for user input, that gives the lower-priority thread plenty oftime to execute, but the user interface can still respond when the user does something to it
compli-These issues can lead to some very confusing bugs Often, these bugs depend on the exact timing of aseries of calculations Execution timing is handled by the operating system and is out of your control so
it can be very hard to reliably reproduce these bugs
The two most problematic types of bugs caused by multi-threading issues are race conditions and deadlocks
Race Conditions
For example, suppose a program uses two threads to calculate new values for a graph The shared variables
old_xand old_yrecord the coordinates of the last value plotted When a thread receives a new Y value, itincrements X, draws a line from the old point to the new one, and updates the old_xand old_yvariables tohold the new point’s coordinates The following pseudocode shows how the threads work:
1: Get new_y2: new_x = old_x + 13: Line (old_x, old_y)-(new_x, new_y)4: old_x = new_x
5: old_y = new_y
Suppose old_x= 10 and old_y= 20 Suppose also that Thread A calculates a new Y value of 25 andThread B calculates a new Y value of 30 The following timeline shows a normal expected execution forthe threads where Thread A executes all of its statements before Thread B executes The result is a linefrom (10, 20) to (11, 25) and another line from (11, 25) to (12, 30)
537
Trang 45B: old_y = 304A: old_x = 11
5A: old_y = 25
In this execution sequence, Thread A starts running its code, but the system interrupts it before it ishes Thread A calculates its new_yand new_xvalues and draws a line from the old point (10, 20) to itsnew point (11, 25)
fin-Now Thread B takes over Because Thread A has not yet updated old_xand old_y, Thread B uses theoriginal values 10 and 20, so it draws a line from (10, 20) to its new point (11, 30) Thread B updates
old_xand old_yto the new coordinates (11, 30)
Thread A resumes and updates old_xand old_yagain, setting them to the coordinates (11, 25) Futurelines start at this point, not from the point (11, 30) saved by Thread B The result is an extra line sticking
up from the graph between points (10, 20) and (11, 30)
Trang 5This type of bug is a type of race condition A race condition occurs when two threads race toward a
com-mon piece of data, and the outcome depends on which thread gets there first In this example, Thread Bupdates the old_xand old_yvariables first, and then Thread A overwrites those values
This problem would be confusing enough if the steps executed in the same order shown in the timelineevery time, but, in general, you don’t know when the operating system will decide that it’s time toswitch between Thread A and Thread B
If you look carefully at the timeline, you can see that the problem occurs when Thread B executesbetween Thread A’s steps 3A and 4A Here, Thread A has drawn its line, but has not yet updated old_x
and old_y If Thread A contains about 100 lines of code, and given no other information, the odds thatThread B will start running between those steps is about 1 in 100 After testing the code 100 times, youwould only have about a 63 percent chance of seeing the bug You would need to test the code around
300 times to have a 95 percent chance of discovering the bug
To calculate the probability of finding the bug in N trials, calculate 1 minus the probability of not ing the bug raised to the power N In this case, 1 – 0.99 ^ 300 is approximately 0.95.
find-At that point, you would know that a bug exists, but it would be very hard to reproduce You cannotsimply step through the code and see what it is doing the way you can with single-threaded code Younot only need to consider what the two threads are doing, but also the exact timing of their execution
One way to avoid this kind of race condition is to make each thread “lock” a variable before using it If thevariable is locked by another thread, the code blocks until the other thread releases its lock before continuing
The following table shows the previous timeline modified by making the threads lock the variables
new_x, new_y, old_x, and old_y
Thread A Thread B
0A: Lock new_x, new_y old_x, old_y1A: new_y = 25
2A: new_x = 113A: (10, 20)-(11, 25)
0B: Lock new_x, new_y old_x, old_y4A: old_x = 11 <blocked>
5A: old_y = 25 <blocked>
6A: Release locks <blocked>
1B: new_y = 302B: new_x = 123B: (11, 25)-(12, 30)4B: old_x = 12
5B: old_y = 306B: Release locks
539
Trang 6In this version, when Thread B starts, it tries to lock the variables and blocks until Thread A releases itslocks That gives Thread A time to finish before Thread B resumes When Thread B resumes, old_xand
old_yhave been updated, so the result is the same as if Thread A had run completely before Thread Bstarted
Deadlocks
Although locks can prevent race conditions, they lead to a new type of multi-threading bug called a
deadlock A deadlock occurs when one thread is blocked, waiting for another thread to release a lock
while the other thread is waiting for the first thread to release a different lock
For example, consider the following timeline Here, Thread A tries to lock variable X, and then variable
Y, while Thread B tries to lock Yand then X
Thread A Thread B
1A: Lock X
1B: Lock Y2B: Lock X2A: Lock Y <blocked>
<blocked> <blocked>
In this example, Thread A locks Xand Thread B locks Y Then Thread B tries to lock Xand blocks becauseThread A already has that variable locked Next, Thread A tries to lock Yand blocks, because Thread Balready has it locked Both threads are blocked waiting for each other, so execution stops
You can fix this simple example by making the two threads try to lock Xand Yin the same order, but inmore complicated examples a solution isn’t always obvious For example, suppose Threads A, B, and Cneed to lock resources X, Y, and Z Thread A starts and locks resource X before the CPU switches to runThread B Thread B locks resource Y and then is interrupted while Thread C takes over Thread C locksresource Z and then tries to lock resources X and Y Because all three resources are locked, all three of thethreads are blocked
Requiring the threads to lock the resources in a specific order works for the three-thread scenarios, aswell If all three threads try to lock the resources in the order X, Y, Z, then whichever thread locksresource X first gets access to the others However, resources don’t always have nicely ordered names, soit’s not always clear what order would be best
Suppose Thread A needs to perform a long task with resource X and short tasks with resources Y and Z
In that case, it makes sense for Thread A to use resource X first so that it keeps the other resources lockedfor the shortest time possible Similarly, suppose Thread B must do a lot of work with resource Y, andThread C must spend a lot of time working with resource Z In this case, there are good reasons for thethreads to grab the resources in different orders If different developers are working independently onthe code for the different threads, they need to coordinate carefully to avoid deadlocks
To make matters worse, suppose the program has half a dozen threads accessing a dozen resources.Suppose some of the threads call library routines that might access the resources You didn’t write the
Trang 7library routines, so you can’t dictate the locking order they use To further confuse the situation, supposethe computer is running other applications that also need to use those resources It would be very diffi-cult to figure out how to coordinate all of these threads, library calls, and processes, some of which areout of your control, so they cannot deadlock.
A different approach to preventing deadlocks is to make lock attempts time out after awhile In the vious example, if Thread B cannot lock variable Ywithin 1 second, it gives up, releases its lock on X, andtries again later That lets Thread A get its lock on Yand continue Later, Thread B tries again and,assuming Thread A has finished and released its locks, it can continue
pre-You can make time-outs a little more effective if they use random durations In this example, it is ble that Thread A would also time out waiting for resource Y, just as Thread B times out waiting for resource X Thread A then locks resource X again and is interrupted, while Thread B locks resource Y again In practice, the operating system will be inconsistent enough to add a little randomness to the process, so this pattern is unlikely to repeat, but it is possible You can reduce the chances slightly by introducing a bit of randomness to the time-out lengths For example, Thread A might time out after 1.2 seconds, while Thread B times out after 0.9 seconds.
possi-As is the case with race conditions, deadlocks depend on the exact execution timing This example onlycauses problems if Thread B runs between steps 1A and 2A Like race conditions, deadlocks are hard toreproduce, but they are a little easier to analyze when they occur because the threads stop running Arace condition leaves behind corrupt data and keeps running, but in a deadlock, you can at least tellwhich threads are stuck
Performance Issues
Multi-threading can improve performance on multi-CPU systems and can improve responsiveness even
on single-CPU systems, but it isn’t free The operating system needs to use extra resources to keep track
of multiple threads It also takes some time to switch from one thread to another
This extra overhead actually slows down total execution, at least on single-CPU systems If you runenough threads, the additional overhead may even slow down multi-CPU systems If you have a lot ofthreads, the system may spend more time switching back and forth than running code
Applications that typically benefit from multi-threading include those that combine relatively slow andrelatively fast operations For example, many applications spend most of their time waiting for the user
to do something The program sits idle until the user clicks a button, selects a menu item, or enters text.While the program is sitting idle, it could be running threads in the background to perform calculations
Similarly, if the program spends a lot of time waiting for network downloads, disk access, Web Servicecalls, and other slow operations, other threads could do something more active at the same time
One technique for dynamically deciding whether it’s worth starting another thread is to check the tem’s CPU load If the CPU is 50 percent used or less, another thread will probably provide a net gain inperformance If CPU usage is higher, such as 75 or 80 percent, adding another thread might degrade per-formance overall
sys-Use the PerformanceCounterclass to check the CPU load You might want to look at a time average over several seconds because the CPU load can fluctuate quickly.
541
Trang 8Using Background Wor kers
To take advantage of multi-threading without stumbling into its pitfalls, it helps to keep things as simple
as possible Although Visual Basic provides tools for building extremely powerful multi-threaded cations, if you aren’t careful, you can quickly build a program that is so complicated and confusing thatyou can’t get it to work If you use multi-threading in the simplest way that can get the job done, youcan minimize your risks
appli-Visual Basic provides a BackgroundWorkercomponent that makes threading easier and safer than it is
if you use the underlying threading tools directly BackgroundWorkerprovides events to perform work,provide the main program with progress reports, and notify the program when it has finished Becausethey are safe and easy, most of the examples presented in this chapter use BackgroundWorkersto man-age threading
During beta testing on one project, we discovered exactly this problem A user could launch a process
by clicking a button One tester found that if he quickly clicked the button again, he could start a second process, so he wrote it up as a bug report He then found that if he clicked really quickly three times, he could make three processes run simultaneously, so he wrote that up separately He went on to spend the afternoon reporting the same bug in about a dozen different ways It probably wasn’t the best use of his testing time, but at least we got to clear a lot of bug reports with a single fix.
The easiest way to handle this sort of problem is to disable the user interface elements that launch thebackground thread while the thread is running In this example, the program should disable the Downloadbutton before starting the thread When the thread finishes, the program should re-enable the button
The second major problem I’ve seen with UI threading is caused by the fact that a thread cannot directlyinteract with controls that it did not create It would be perfectly natural for a thread that performs somecalculation to display periodic progress messages and its final result on the main program’s controls.Unfortunately, that isn’t allowed
Instead, the thread must use the Invokemethod to call a routine defined on some control Invoke
passes the call to the thread that created the control so that it can execute the routine
Trang 9Usually a developer adds an update routine to the form and then uses Invoketo call that routine Theupdate routine can then access any control on the form.
Example program BackgroundGraphs(shown in Figure 21-1 and available for download at helper.com/one_on_one.htm) uses four BackgroundWorkercontrols to display four graphs Each
www.vb-BackgroundWorkeruses Invoketo make the main UI thread update the corresponding graph
Figure 21-1: Program BackgroundGraphsdisplays four graphs withdifferent shapes running at different speeds
The following code shows how program BackgroundGraphsloads and starts its BackgroundWorker
controls (You can download this example at www.vb-helper.com/one_on_one.htm.) To make ing the graphs easier, the form’s Loadevent handler saves references to their PictureBoxesin the
updat-m_Graphsarray It also makes Bitmapsto hold each graph’s image The code then sets initial data ues for the four graphs and calls the BackgroundWorkers’ RunWorkerAsyncmethods to make themstart running asynchronously
val-‘ The graphing PictureBoxes
Private m_Graphs() As PictureBoxPrivate m_Wid As Integer
Private m_Hgt As Integer
‘ The graphs’ images
Private m_Bitmaps() As BitmapPrivate m_LastY() As IntegerPrivate Sub Form1_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.Load
‘ Save the PictureBoxes in an array
m_Graphs = New PictureBox() {picGraph0, picGraph1, picGraph2, picGraph3}
‘ Get the PictureBoxes’ dimensions
543
Trang 10m_Wid = picGraph0.ClientSize.Widthm_Hgt = picGraph0.ClientSize.Height
‘ Make the bitmaps
Dim num_graphs As Integer = m_Graphs.LengthReDim m_Bitmaps(0 To num_graphs - 1)ReDim m_LastY(0 To num_graphs - 1)For i As Integer = 0 To 3
m_Bitmaps(i) = New Bitmap(m_Wid, m_Hgt)Using gr As Graphics = Graphics.FromImage(m_Bitmaps(i))gr.Clear(Color.White)
End Usingm_Graphs(i).Image = m_Bitmaps(i)Next i
‘ Start the background workers
m_LastY(0) = Y0(0)bgwGraph0.RunWorkerAsync()m_LastY(1) = m_Hgt \ 2bgwGraph1.RunWorkerAsync()m_LastY(2) = m_Hgt \ 2bgwGraph2.RunWorkerAsync()m_LastY(3) = Y3(0)
bgwGraph3.RunWorkerAsync()End Sub
When the program calls a BackgroundWorker’s RunWorkerAsyncmethod, the worker’s DoWorkeventfires The event handler should perform whatever task the worker is supposed to handle
The following code shows how the first worker generates its graph The other graphs work similarly, sotheir code isn’t shown here
‘ Draw graph 0
Private Sub bgwGraph0_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgwGraph0.DoWork
‘ Work at reduced priority
Thread.CurrentThread.Priority = ThreadPriority.BelowNormalStatic X As Integer = 0
‘ See if we should cancel
If bgwGraph0.CancellationPending Then Exit Do
Trang 11LoopEnd SubPrivate Function Y0(ByVal x As Integer) As IntegerReturn CInt(m_Hgt / 2 + 10 * Sin(x / 11) + 5 * Sin(x / 7))End Function
The code first reduces the priority for the new BackgroundWorker’s thread That allows the UI thread
to run at a higher priority, so it can respond quickly to user events
This example also uses Sleepcalls to allow the UI thread to make progress If you don’t lower the ground threads’ priority and you don’t call Sleep, the threads hog the CPU, and the UI thread cannot respond to user events For an interesting exercise, comment out the Priorityand Sleepcode and see what happens.
back-Next, the code enters a loop where it calculates graph values It increments the graph’s X coordinate, culates the corresponding new Y value, and uses the form’s Invokemethod to call the m_PlotPointDelegate(described shortly) It passes this routine the index of the graph and the new Y coordinate.The code then sleeps for 1 millisecond This makes the system take control away from this thread If youdon’t do this, the thread tends to hog the CPU, and the other threads don’t make any headway
cal-Eventually, the system decides the thread has had its share and runs another thread The result is thatone thread runs for awhile and the others are stuck Then a new thread runs while the rest are stuck Theillusion of simultaneously running threads is lost
You may get different results if your computer has multiple CPUs For another interesting exercise, comment out the Sleepcalls and see what happens.
Next, the DoWorkevent handler checks the BackgroundWorker’s CancellationPendingproperty tosee if the code has asked the worker to stop If CancellationPendingis True, the code exits its Do
loop, so it stops drawing the graph
The following code shows how the main UI thread updates the graphs It first defines PlotPointDelegateto be a delegate referring to a subroutine that takes two integer parameters Next, it declaresvariable m_PlotPointDelegateto be of this type and sets the variable to point to the PlotPoint
subroutine
‘ Plot a data point
Private Delegate Sub PlotPointDelegate(ByVal graph_num As Integer, _ByVal Y As Integer)
Private m_PlotPointDelegate As PlotPointDelegate = AddressOf Me.PlotPointPrivate Sub PlotPoint(ByVal graph_num As Integer, ByVal Y As Integer)
‘ Scoot the image over to the left
Dim bm As New Bitmap(m_Wid, m_Hgt)Using gr As Graphics = Graphics.FromImage(bm)gr.DrawImage(m_Bitmaps(graph_num), -1, 0)
‘ Clear the right edge
gr.DrawLine(Pens.White, m_Wid - 1, 0, m_Wid - 1, m_Hgt)
‘ Draw the new point
545
Trang 12gr.DrawLine(Pens.Blue, m_Wid - 2, m_LastY(graph_num), m_Wid - 1, Y)End Using
‘ Save the new Y value
m_LastY(graph_num) = Y
‘ Refresh the graph
m_Bitmaps(graph_num) = bmm_Graphs(graph_num).Image = bmEnd Sub
When a BackgroundWorkerinvokes m_PlotPointDelegate, the main UI thread calls subroutine
PlotPoint That routine makes a new Bitmapfor the appropriate graph It copies the existing Bitmap
onto the new one, shifting the image one pixel to the left It erases the rightmost column of pixels anddraws a line from the previous data point to the new one It then saves the new data point and displaysthe new Bitmapin the appropriate PictureBox
The last piece of code in program BackgroundGraphslets the user start and stop a graph The followingcode shows how the program starts and stops the first graph’s BackgroundWorker The program con-trols the others similarly
‘ Stop a worker
Private Sub btnStop0_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnStop0.Click
ToggleWorker(btnStop0, bgwGraph0)End Sub
Private Sub ToggleWorker(ByVal btn As Button, ByVal bgw As BackgroundWorker)
If bgw.IsBusy Thenbgw.CancelAsync()btn.Text = “Start”
Elsebgw.RunWorkerAsync()btn.Text = “Stop”
End IfEnd Sub
End Class
When the user clicks the first graph’s Stopbutton, the program calls subroutine ToggleWorker Thatroutine checks a BackgroundWorker If the worker’s IsBusyproperty indicates that it is running, thecode calls its CancelAsyncmethod to set the worker’s CancellationPendingproperty to True
If you want to call a BackgroundWorker’s CancelAsyncmethod, you must set its
WorkerSupportsCancellationproperty to True.
If the worker’s IsBusyproperty indicates that it is not running, the code calls the worker’s
RunWorkerAsyncmethod to start it working again
Following are the three main keys to this example:
❑ Lower the background workers’ priority so that the user interface thread has precedence
❑ Use Sleepto force the threads to give up control of the CPU so that they all get turns