Multithreading in Windows Forms

The ThreadGui application examines some of the threading issues that you have to face in a Windows Forms program. Multithreading can be very useful in this environment because it lets you keep the user interface responsive to the end user while you're doing intensive work, and you can also use it to let the end user cancel a long-running task. The drawback is that you need to respect the single-threaded nature of a Windows Form.

If you load the ThreadGui solution into Visual Studio and execute it by pressing F5, you'll see the application's user interface, as shown in Figure 14-6.

click to expand
Figure 14-6: The user interface of the ThreadGui application

When you enter a number into the top text box and click the button marked Accumulate, the program accumulates a running total by adding every number together between 1 and the number entered in the text box. So if you entered 5 into the top text box, the accumulated total would be 15 (1 + 2 + 3 + 4 + 5). The Cancel button allows you to interrupt and stop the accumulation calculation at any time. The label at the bottom of the form displays the results of the calculation.

The second text box is used to allow the user to monitor progress of the calculation. After the accumulation is performed the number of times specified in this text box, the current running total is displayed in the label at the bottom of the form and the display is paused for 0.1 second so that the user can glimpse this intermediate result. Then the calculation continues accumulating until either the next display interval is reached or the calculation finishes.

The challenge is to run the potentially lengthy calculation while keeping the user interface responsive so that the user can cancel the calculation and while displaying the intermediate results of the calculation in a safe manner.

The first major problem to overcome is that you should never, ever, update a control (in this case, the label at the bottom of the form) from a thread other than the thread that created the control. If you break this Windows Forms law, your program will experience strange and difficult multithreading bugs, and you won't be able to fix these bugs except by changing your program to obey the law. The first approach that many developers experiment with when faced with this prohibition is to tell the calculation thread to raise an event that the form can handle and use to update the user interface. Unfortunately, as mentioned earlier in this chapter, this won't work. An event handler always runs on the same thread that raised the event, so the event handler also isn't allowed to update the user interface.

The second problem is for the user interface thread to find a way of telling the calculation thread that the user has canceled the calculation request. Often a developer will think about setting a class-level variable that can be accessed and shared by both threads. This is, however, difficult to do without running into the thread synchronization issues that I discussed in the previous section.

Listing 14-11 shows the code that launches the worker thread to do the calculation once the user has clicked the Accumulate button. It uses an asynchronous delegate to spawn a work request that will be handled by the .NET thread pool. The delegate's BeginInvoke method is used to start the calculation thread asynchronously and pass it the specified number to accumulate.

Listing 14-11. Code to Launch the Calculation Thread
start example
 Option Strict On Imports System.Threading Public Class MainForm : Inherits System.Windows.Forms.Form     Private Delegate Sub CalcDelegate(ByVal AnyNumber As Int32)     Private Delegate Sub _                  ProgressDelegate(ByVal CurrentTotal As Decimal, _                                     ByVal NumberReached As Int32, _                                     ByRef CancelRequest As Boolean)     Private m_CancelRequested As Boolean = False     Private Sub ButtonCalc_Click(ByVal sender As System.Object, _                                    ByVal e As System.EventArgs) _                                    Handles cmdCalc.Click         'Init calculation         Me.cmdCalc.Enabled = False         Me.cmdCancel.Enabled = True         m_CancelRequested = False         'Use asynch delegate to launch thread from thread pool         Dim CalcAccumulation As CalcDelegate = New _                       CalcDelegate(AddressOf CalculateAccumulation)         CalcAccumulation.BeginInvoke(_                       Convert.ToInt32(Me.txtNumber.Text), _                       AddressOf CalcComplete, Nothing)     End Sub 
end example

If you don't need the control that a manual thread gives you, such as setting the thread name or priority, using the thread pool spares you from the messy details of thread management and scales better in many multithreaded environments. Even better, as you'll see shortly, it's easy to propagate background thread exceptions back to the main thread when using the thread pool.

Listing 14-12 shows the method that runs the calculation thread. First place a breakpoint on line 162 (the line marked in bold in Listing 14-12) and then run the application by pressing F5. When you click the Accumulate button, the program will break as soon as it reaches your breakpoint. If you now look at the Threads window, you'll see two threads: the user interface thread and the calculation thread from the thread pool.

Listing 14-12. Performing the Accumulation Calculation
start example
 Private Sub CalculateAccumulation(_              ByVal NumberToAccumulate As Int32)     Dim CalcObject As New Calc(NumberToAccumulate), _         CurrentTotal As Decimal = 0     Dim CancelRequested As Boolean = False     With CalcObject  Do While.NumberReached < = NumberToAccumulate  CurrentTotal = _                  .Accumulate(Convert.ToInt32(Me.txtInterval.Text))               ShowProgress(CurrentTotal,.NumberReached, _                           CancelRequested)             If CancelRequested = True Then                 Exit Do             End If         Loop     End With End Sub 
end example

After performing each stage of the accumulation, the calculation thread calls the ShowProgress method, which is shown in Listing 14-13. This method is where the clever work happens. Remember that you should never update a user interface control from any thread except the one that created the control. To verify whether the user interface thread or the calculation thread is trying to update the user interface, the ShowProgress method checks Me.InvokeRequired . This will return True if the current thread isn't the user interface thread, and it will return False if it's the user interface thread. If InvokeRequired is False , then the thread is allowed to update the user interface directly, and therefore update the label with information about the progress of the calculation.

Listing 14-13. Updating the User Interface with Intermediate Calculation Results
start example
 Private Sub ShowProgress(ByVal CurrentAccumulation As Decimal, _                            ByVal NumberReached As Int32, _                            ByRef CancelRequest As Boolean)      If Me.InvokeRequired = True Then         'Transfer to GUI thread to show progress         Dim CancelRequested As Object = False         Dim SP As ProgressDelegate = _             New ProgressDelegate(AddressOf ShowProgress)         Dim Arguments() As Object = New Object() _                    {CurrentAccumulation, _                    NumberReached, _                    CancelRequested}         Me.Invoke(SP, Arguments)         CancelRequest = DirectCast(CancelRequested, Boolean)     Else         'We're on the GUI thread, so just show progress         With Me.lblResult             .Text = "Number reached: " & NumberReached.ToString             .Text += Environment.NewLine             .Text += "Accumulated total: " _                   & CurrentAccumulation.ToString         End With         'Pause for a short time to allow user to read display         Thread.CurrentThread.Sleep(100)         'Return any cancellation request         CancelRequest = m_CancelRequested     End If End Sub 
end example

The interesting work happens when InvokeRequired is True , and therefore the user interface can't be updated directly. Every control has an Invoke method, and this is one control method that the CLR guarantees is safe to call from any thread. The arguments for the Invoke method include a delegate and a developer-defined set of arguments that are used to call the delegate's associated method. So this code calls ShowProgress recursively using the ProgressDelegate delegate. Using the Invoke method ensures that the recursive call happens on the user interface thread, where it's safe to update the user interface.

This technique for updating the user interface with progress information from the calculation thread works well and avoids any updating of the user interface from a nonuser interface thread. Instead of interacting directly, the user interface and calculation threads pass messages to each other, which means that you never have to worry about any thread synchronization or deadlock issues.

So the first problem of updating the user interface is solved , but you still need to tackle the problem of canceling the thread if the user clicks the Cancel button. The code behind the Cancel button is shown in Listing 14-14. It sets a class-level variable signifying that the user has issued a cancellation request. But how can you give the calculation thread access to this variable without running into thread synchronization issues?

Listing 14-14. Canceling the Calculation
start example
 Private Sub ButtonCancel_Click(ByVal sender As System.Object, _                                  ByVal e As System.EventArgs) _                                  Handles cmdCancel.Click     'Request that calculation thread cancels itself     m_CancelRequested = True End Sub 
end example

The key to this puzzle lies in the ShowProgress method shown in Listing 14-13. This method has a ByRef argument called CancelRequest . When this method is called on the user interface thread, it sets this argument to the class-level request cancellation variable. Because the calculation thread regularly calls the ShowProgress method to update the user interface with its progress, it can read the cancellation request argument after it's called the Invoke method. This allows the request for cancellation to pass safely from the user interface thread to the calculation thread without having to worry about synchronization issues. Yet again, a message passing between the two threads prevents any problems that might arise if the two threads interacted directly.

Finally, once the calculation thread has completed, it invokes the callback that it was passed when it was started. This is the CalcComplete method shown in Listing 14-15, which simply resets the user interface so that another request can be started.

Listing 14-15. Completing the Calculation
start example
 Private Sub CalcComplete(ByVal CalcResult As System.IAsyncResult)          'Called when asynch thread completes          Me.cmdCalc.Enabled = True          Me.cmdCancel.Enabled = False End Sub 
end example

Comprehensive VB .NET Debugging
Comprehensive VB .NET Debugging
ISBN: 1590590503
EAN: 2147483647
Year: 2003
Pages: 160
Authors: Mark Pearce © 2008-2017.
If you may any questions please contact us: