Long-Running Operations


Imagine that the value of pi in System.Math.PI, at only 20 digits, just isn't accurate enough for you. In that case, you may find yourself writing an application like the one in Figure 14.1 to calculate pi to an arbitrary number of digits.

Figure 14.1. Digits of Pi Application

This program takes as input the number of digits of pi to calculate and, when the Calc button is pressed, shows the progress as the calculation happens.

Progress Indication

Although most applications don't need to calculate the digits of pi, many kinds of applications need to perform long-running operations, whether it's printing, making a Web service call, or calculating the interest earnings of a certain billionaire in the Pacific Northwest. Users are generally content to wait for such things as long as they can see that progress is being made. That's why even this simple application has a progress bar.

The algorithm it uses calculates pi 9 digits at a time. As each new set of digits is available, the application updates the text and the progress bar. For example, Figure 14.2 shows progress on the way to calculating 1,000 digits of pi (if 21 digits are good, then 1,000 must be better).

Figure 14.2. Calculating Pi to 1,000 Digits

The following shows how the UI is updated as the digits of pi are calculated:

 
 Class AsyncCalcPiForm   Inherits Form   ...   Sub ShowProgress(pi As String, _       totalDigits As Integer, digitsSoFar As Integer)       piTextBox.Text = pi       piProgressBar.Maximum = totalDigits       piProgressBar.Value = digitsSoFar   End Sub   Sub CalcPi(digits As Integer)       Dim pi As StringBuilder = New StringBuilder("3", digits + 2)       ' Show progress       ShowProgress(pi.ToString(), digits, 0)       If digits > 0 Then          pi.Append(".")          Dim i As Integer          For i = 0 to digits - 1 Step 9              Dim nineDigits As Integer = _                  NineDigitsOfPi.StartingAt(i+1)              Dim digitCount As Integer = _                  Math.Min(digits  i, 9)              Dim ds As String = _                  String.Format("{0:D9}", nineDigits)              pi.Append(ds.Substring(0, digitCount))              ' Show progress              ShowProgress(pi.ToString(), _                  digits, i + digitCount)           Next       End If   End Sub   Sub calcButton_Click(sender As Object, e As EventArgs)       CalcPi(CInt(digits.Value))   End sub End Class 

This implementation works just fine for a small number of digits. But suppose the user switches away from the application and then returns in the middle of calculating pi to a large number of digits, as shown in Figure 14.3.

Figure 14.3. No Paint for You!

The problem is that the application has a single thread of execution (this kind of application is often called a single-threaded application ), so while the thread is calculating pi, it can't also be drawing the UI. This didn't happen before the user switched the application to the background because both the text box and the progress bar force their own painting to happen immediately as their properties are set (although the progress bar seems to be better at this than the text box). However, after the user puts the application into the background and then the foreground again, the main form must paint the entire client area, and that means processing the Paint event. Because no other event can be processed until the application returns from the Click event on the Calc button, the user won't see any display of further progress until all the digits of pi are calculated.

What this application needs is a way to free the UI thread to do UI work and handle the long-running pi calculation in the background. For this, it needs another thread of execution.

Asynchronous Operations

A thread of execution (often called simply a thread ) is a series of instructions and a call stack that operate independently of the other threads in the application or in any other application. In every version of Windows since Windows 95, Windows schedules each thread transparently so that a programmer can write a thread almost (but not quite) as if it were the only thing happening on the system. Starting a thread is an asynchronous operation, in that the current thread of execution will continue immediately, executing independently of the new thread. To start a new thread of execution in .NET is a matter of creating a Thread object from the System.Threading namespace, passing a delegate [1] as the constructor parameter, and starting the thread:

[1] You can read more about delegates in Appendix B: Delegates and Events.

 
 Imports System.Threading ... Class AsyncCalcPiForm   Inherits Form   ...   Dim digitsToCalc As Integer = 0   Sub CalcPiThreadStart()       CalcPi(digitsToCalc)   End Sub   Sub calcButton_Click(sender As Object, e As EventArgs)       digitsToCalc = CInt(digits.Value)       Dim piThread As Thread = _           New Thread(New ThreadStart(_             AddressOf CalcPiThreadStart))       piThread.Start()   End Sub End Class 

This code creates a new thread and begins execution of the thread by passing a delegate wrapper around the method to call. Now, instead of waiting for CalcPi to finish before returning from the button Click event, the UI thread spawns a worker thread, immediately returning the UI thread to its user interaction duties . Figure 14.4 shows the two threads doing their separate jobs.

Figure 14.4. Na ve Multithreading

Spawning a worker thread to calculate pi leaves the UI thread free to handle events (which WinForms creates as it takes messages off the Windows message queue). When the worker thread has more digits of pi to share with the user, it directly sets the values of the text box and the progress bar controls. (Actually, letting the worker thread access controls created by the UI thread is dangerous, but we'll get to that a little later.)

In the sample code to start a thread, notice that no arguments are passed to the worker thread's entry point, CalcPiThreadStart. This is because the delegate that is used to construct the Thread class allows no arguments to be passed. Instead, we tuck the number of digits to calculate into a field, digitsToCalc. Then we call the thread entry point, which uses digitsToCalc to call the CalcPi method in turn . Because you can't pass arguments to the thread start method, we prefer to use custom delegates for spawning threads. In addition, an asynchronous delegate will be handled on a thread from the per-process thread pool, something that scales better than does creating a new thread for each of a large number of asynchronous operations.

Here's how to declare a custom delegate suitable for calling CalcPi:

 
 Delegate Sub CalcPiDelegate(digits As Integer) 

After the custom delegate has been defined, the following creates an instance of the delegate to call the CalcPi method synchronously:

 
 Sub calcButton_Click(sender As Object, e As EventArgs)   Dim calcPi As CalcPiDelegate = New CalcPiDelegate(AddressOf CalcPi)   calcPi(CInt(digitsUpDown.Value)) End Sub 

Because calling CalcPi synchronously is the cause of our trouble, we need to call it asynchronously. Before we do that, however, we need to understand a bit more about how delegates work. The CalcPiDelegate declaration implicitly declares a new class derived from MultiCastDelegate with three methods : Invoke, BeginInvoke, and EndInvoke:

 
 Class CalcPiDelegate   Inherits MulticaseDelegate   Sub Invoke(digits As Integer)   Sub BeginInvoke(digits As Integer, _       callback As AsyncCallback, asyncState As Object)   Sub EndInvoke(result As IAsyncResult) End Class 

When the application created an instance of CalcPiDelegate and called it like a method, it was really calling the synchronous Invoke method, which simply turned around and called the CalcPi method on the same thread. BeginInvoke and EndInvoke, however, are the pair of methods that allow asynchronous invocation of a method on a new thread for a per-process pool of threads. To have the CalcPi method called on another thread ”the aforementioned worker thread ”the application uses BeginInvoke:

 
 Sub calcButton_Click(sender As Object, e As EventArgs)   Dim calcPi As CalcPiDelegate = _       New CalcPiDelegate(AddressOf CalcPi)   calcPi.BeginInvoke(CInt(digitsUpDown.Value), Nothing, Nothing) End Sub 

Notice the nulls for the last two arguments of BeginInvoke. We would need these arguments if we needed to later harvest the result from the method we're calling (which is also what EndInvoke is for). Because the CalcPi method updates the UI directly, we don't need anything but nulls for these two arguments.

At this point, we've got an application with a fully interactive UI that shows progress on a long-running operation. Unfortunately, we're not quite finished yet.

Multithreaded Safety

As it turns out, we're lucky that this application works at all. Because we start the CalcPi method on a worker thread, when CalcPi calls the ShowProgress method it's accessing the text box and progress bar controls from the worker thread, even though those controls were created on the UI thread. This violates a key requirement that's been present since Windows first got support for threads: "Thou shalt operate on a window only from its creating thread." In fact, the WinForms documentation is clear on this point: "There are four methods on a control that are safe to call from any thread: Invoke, BeginInvoke, EndInvoke, and CreateGraphics. For all other method calls, you should use one of the invoke methods to marshal the call to the control's thread." So when the CalcPi method calls the ShowProgress method, which in turn accesses two controls created by the UI thread, our application is clearly violating this rule.

Luckily, long-running operations are common in Windows applications. As a result, each UI class in WinForms ”meaning every class that ultimately derives from System.Windows.Forms.Control ”has a property that you can use to find out whether it's safe to act on the control from that thread. The property is InvokeRequired, which returns true if the calling thread needs to pass control to the UI thread before calling a method on the control. A simple Assert in the ShowProgress method would have immediately shown the error in our sample application:

 
 Imports System.Diagnostics Sub ShowProgress(pi As String, totalDigits As Integer, digitsSoFar As     Integer)   ' Make sure we're on the UI thread   Debug.Assert(Me.InvokeRequired = False)   ... End Sub 

Because the worker thread is clearly not allowed to show progress directly, we need to pass control from the worker thread back to the UI thread. From the names of the first three methods that are safe to call from any thread ”Invoke, BeginInvoke, and EndInvoke ”it should be clear that you'll need another delegate to pass control appropriately. The delegate will be created on the worker thread and then executed on the UI thread so that we can have safe, single-threaded access to UI objects.

Synchronous Callbacks

Asynchronous operations, such as the call to a delegate's BeginInvoke method, return immediately, so they are nonblocking . This means that the thread isn't blocked waiting for the method to complete. Synchronous operations, on the other hand, are blocking , because they do cause the calling thread to block until the method returns.

Depending on the blocking behavior you're interested in, you can call Invoke or BeginInvoke to block or not to block, respectively, when calling into the UI thread:

 
 Class System.Windows.Forms.Control   Overloads Function Invoke(method As Delegate) As Object   NotOverridable Overloads Function Invoke(method As Delegate, _       args() As Object) As Object   Overloads Function BeginInvoke(method As Delegate) As IAsyncResult   NotOverridable Overloads Function BeginInvoke(method As Delegate, _       args() As Object) As IAsyncResult   NotOverridable Function EndInvoke(asyncResult As IAsyncResult) As Object End Class 

Control.Invoke will block until the UI thread has processed the request: that is, until Control.Invoke has put a message on the UI thread's message queue and has waited for it to be processed like any other message (except that the delegate's method is called instead of an event handler). Because Invoke takes a Delegate argument, which is the base class for all delegates, it can form a call to any method, using the optional array of objects as arguments and returning an object as the return value for the called method. Using Control.Invoke looks like this:

 
 Sub ShowProgress(pi As String, totalDigits As Integer, digitsSoFar As Integer)   ... End Sub Delegate Sub ShowProgressDelegate(pi As String, totalDigits As Integer, _   digitsSoFar As Integer) Sub CalcPi(digits As Integer)   Dim pi As StringBuilder = New StringBuilder("3", digits + 2)   ' Get ready to show progress   Dim showProgress As ShowProgressDelegate = _       New ShowProgressDelegate(AddressOf ShowProgress)   ' Show progress   Me.Invoke(showProgress, New Object() { pi.ToString(), digits, 0})   If digits > 0 Then      pi.Append(".")      Dim i As Integer      For i = 0 to digits  1 Step 9          ...          ' Show progress          Me.Invoke(showProgress, New Object() _          { pi.ToString(), digits, i + digitCount})      Next   End If End Sub 

Notice the declaration of a new delegate, ShowProgressDelegate. This delegate matches the signature of the ShowProgress method we'd like to be called on the UI thread. Because ShowProgress takes three arguments, the code uses an overload to Invoke that takes an array of objects to form the arguments to the ShowProgress method.

Now the UI thread uses a delegate, calling Delegate.BeginInvoke to spawn a worker thread, and the worker thread uses Control.Invoke to pass control back to the UI thread when the progress controls need updating. Figure 14.5 shows our safe multithreading architecture.

Figure 14.5. Safe Multithreading

Notice that Figure 14.5 shows only the UI thread ever touching the controls and shows that the worker thread uses the message queue to let the UI thread know when progress needs reporting.

Asynchronous Callbacks

Our use of the synchronous call to Control.Invoke works just fine, but it gives us more than we need. The worker thread doesn't get any output or return values from the UI thread when calling through the ShowProgressDelegate. By calling Invoke, we force the worker thread to wait for the UI thread, blocking the worker thread from continuing its calculations. This is a job tailor-made for the asynchronous Control.BeginInvoke method:

 
 Sub CalcPi(digits As Integer)   Dim pi As StringBuilder = New StringBuilder("3", digits + 2)   ' Get ready to show progress   Dim showProgress As ShowProgressDelegate = _       New ShowProgressDelegate(ShowProgress)   ' Show progress   Me.BeginInvoke(showProgress, New Object() {pi.ToString(), _ digits, 0})   If digits > 0 Then       pi.Append(".")       Dim i As Integer       For i = 0 to digits  1 Step 9           ...           ' Show progress           Me.BeginInvoke(showProgress, _               New Object() {pi.ToString(), _               digits, i + digitCount})       Next   End If End Sub 

Notice that the only thing different in this code is the call to BeginInvoke instead of Invoke. All the arguments are the same, and we can ignore the return value from BeginInvoke. The BeginInvoke method returns an IAsyncResult interface, which provides access to the current status of the request and allows the worker thread to harvest any results. Because there are no results to harvest, there's no need to worry about the IAsyncResult or ever calling EndInvoke.

If you need to retrieve results from the UI thread to the worker thread, you can:

 
 ' Call back to the UI thread Dim res As IAsyncResult = _      Me.BeginInvoke(someDelegateWithResults, ...) ' Check for results Do While(res.IsCompleted = False)   System.Threading.Thread.Sleep(100) Loop ' Harvest results Dim methodResults As Object = Me.EndInvoke(res) ' Do something with results ... 

IAsyncResult.IsCompleted is meant to be called periodically while doing other things (such as during the Application class's Idle event, mentioned in Chapter 11: Applications and Settings). Of course, if you're doing nothing except polling for results, the Control.Invoke method is what you want.

Simplified Multithreading

The call to BeginInvoke is a bit cumbersome, especially because it happens twice in the CalcPi method. We can simplify things by updating the ShowProgress method to make the asynchronous call itself. If ShowProgress is called from the UI thread, it will update the controls, but if it's called from a worker thread, it uses BeginInvoke to call itself back on the UI thread. This lets the application go back to the earlier, simpler CalcPi implementation:

 
 Sub ShowProgress(pi As String, totalDigits As Integer, digitsSoFar As Integer)   ' Make sure we're on the UI thread   If Me.InvokeRequired = False Then       piTextBox.Text = pi       piProgressBar.Maximum = totalDigits       piProgressBar.Value = digitsSoFar   Else       ' Show progress asynchronously       Dim showProgress As ShowProgressDelegate = _           New ShowProgressDelegate(AddressOf ShowProgress)       Me.BeginInvoke(showProgress, New Object() {pi, _           totalDigits, digitsSoFar})   End If End Sub Sub CalcPi(digits As Integer)   Dim pi As StringBuilder = New StringBuilder("3", digits + 2)   ' Show progress   ShowProgress(pi.ToString(), digits, 0)   If digits > 0 Then      pi.Append(".")      Dim i As Integer      For i = 0 to digits  1 Step 9          ...          ' Show progress          ShowProgress(pi.ToString(), digits, i + digitCount)      Next  End If End Sub 

When you build multithread UI code, it's common for the methods that interact with the controls to check that they're running on the UI thread. During development, you may need a way to track down places where you've forgotten to make the appropriate transition between threads. To make those places obvious, we recommend that in every method that might be called from a worker thread, use a call that asserts that InvokeRequired is not required.

Canceling

So far, the sample application can send messages back and forth between the worker and the UI threads without a care in the world. The UI thread doesn't have to wait for the worker thread to complete or even be notified on completion, because the worker thread communicates its progress as it goes. Similarly, the worker thread doesn't have to wait for the UI thread to show progress as long as progress messages are sent at regular intervals to keep users happy.

However, one thing doesn't make users happy: not having full control of any processing that their applications are performing. Even though the UI is responsive while pi is being calculated, users would still like the option to cancel the calculation if they've decided they really need 1,000,001 digits and they mistakenly asked for only 1,000,000. Figure 14.6 shows an updated UI for CalcPi that allows cancellation.

Figure 14.6. Letting the User Cancel a Long-Running Operation

Implementing cancel for a long-running operation is a multistep process. First, you need to provide a UI that lets the user cancel the operation. In this case, the Calc button is changed to a Cancel button after the calculation has begun. Another popular choice is to display a separate progress dialog, which typically includes current progress details, a progress bar showing percentage of work complete, and a Cancel button.

If the user decides to cancel, that is noted in a member variable. In addition, the UI is disabled for the short time between the time when the UI thread knows the worker thread should stop and the time the worker thread itself knows and has had a chance to stop sending progress. If you ignore this lag time, the user could start another operation before the first worker thread stops sending progress, making it the job of the UI thread to figure out whether it's getting progress from the new worker thread or the old worker thread, which is supposed to be shutting down.

You could assign each worker thread a unique ID so that the UI thread can keep such things organized (and, in the face of multiple simultaneous long-running operations, you may well need to do this), but it's often simpler to pause the UI for this brief amount of time. The sample application keeps track of its current processing state using a value from a three-value enumeration:

 
 Enum CalcState   Pending ' No calculation running or canceling   Calculating ' Calculation in progress   Canceled ' Calculation canceled in UI but not worker End Enum Dim state As CalcState = CalcState.Pending 

Now, depending on what state the application is in, it treats the Calc button differently:

 
 Sub calcButton_Click(sender As Object, e As EventArgs)   ' Calc button does double duty as Cancel button   Select Case state       ' Start a new calculation       Case CalcState.Pending           ' Allow canceling           state = CalcState.Calculating           calcButton.Text = "Cancel"           ' Async delegate method           Dim calcPi As CalcPiDelegate = _               New CalcPiDelegate(AddressOf CalcPi)           calcPi.BeginInvoke(CInt(digitsUpDown.Value), _               Nothing, Nothing)       ' Cancel a running calculation       Case CalcState.Calculating           state = CalcState.Canceled           calcButton.Enabled = False        ' Shouldn't be able to press Calc button while it's canceling        Case CalcState.Canceled            Debug.Assert(False)    End Select End Sub 

After the UI thread has communicated with the worker thread that the operation has been canceled, the UI thread enables the UI again and resets the state back to Pending (shown later) so that the user can start another operation. To communicate to the worker that it should cancel, the sample augments the ShowProgress method to include a new ByRef parameter:

 
 Sub ShowProgress(..., ByRef cancel As Boolean)   ... End Sub Sub CalcPi(digits As Integer)   Dim cancel As Boolean = False   ...   Dim i As Integer   For i = 0 to digits  1 Step 9       ...       ' Show progress       ShowProgress(..., ByRef cancel)       If cancel Then Exit For   Next End Sub 

You may be tempted to make the cancel indicator a Boolean return value from ShowProgress, but we find it hard to remember whether a return value of "true" means to cancel or to continue as normal. We use the out parameter technique to make it very clear what's going on.

The only thing left to do is to update the ShowProgress method to notice whether the user has asked to cancel and to let CalcPi know accordingly . Exactly how that information is communicated depends on which technique we'd like to use.

Communication with Shared Data

Remember that the ShowProgress method is the code that actually performs the transition between the worker thread and the UI thread, so it's the one at the heart of the communication between the two threads. The obvious way to communicate the current state of the UI is to give the worker thread direct access to the state member variable:

 
 Sub ShowProgress(..., ByRef cancel As Boolean)   If state = CalcState.Canceled Then       State = CalcState.Pending       cancel = True       Exit Sub   End If   ... End Sub 

I hope that something inside you cringed when you saw this code. If you're going to do multithreaded programming, you're going to have to watch out for any time that two threads have simultaneous access to the same data (in this case, the state member variable). Shared access to data between threads makes it very easy to get into race conditions, in which one thread is racing to read data that is only partially up-to-date before another thread has finished updating it. For concurrent access to shared data to work, you must monitor usage of your shared data to make sure that each thread waits patiently while the other thread works on the data.

 
 Dim stateLock As Object = New Object() Sub ShowProgress(..., ByRef cancel As Boolean)   SyncLock stateLock) ' Monitor the lock       If state = CalcState.Cancel Then           state = CalcState.Cancel           cancel = True           Exit Sub       End If   End SyncLock End Sub 

The data has now been properly protected against race conditions, but the way we've done it invites another problem known as a deadlock . When two threads are deadlocked, each of them waits for the other to complete its work before continuing, thereby ensuring that neither will actually progress.

If all this talk of race conditions and deadlocks has caused you concern, that's good. Multithreaded programming with shared data is hard. So far we've been able to avoid these issues because we have passed copies of data around so that no two threads need to share access to any one piece of data. If you don't have shared data, there's no need for synchronization. If you find that you need access to shared data ”maybe because the overhead of copying the data is too great a space or time burden ”then you'll need to read up on multithreading and shared data synchronization, topics that are beyond the scope of this book.

Luckily, the vast majority of multithreading scenarios, especially as related to UI multithreading, seem to work best with the simple message-passing scheme used so far. Most of the time, you don't want the UI to have access to data being worked on in the background, such as the document being printed or the collection of objects being enumerated. For these cases, passing data to be owned by the receiving thread is just the ticket.

Communicating via Method Parameters

Because ShowProgress has already been updated with an out parameter, the sample lets it check the state variable when it's executing on the UI thread:

 
 Sub ShowProgress(..., ByRef cancel As Boolean)   ' Make sure we're on the UI thread   If Me.InvokeRequired Then       ...       ' Check for cancel       cancel = (state = CalcState.Canceled)       ' Check for completion       If (cancel OrElse (digitsSoFar = totalDigits))           state = CalcState.Pending           calcButton.Text = "Calc"           calcButton.Enabled = True       End If   Else       ' Transfer control to the UI thread       ...   End If End Sub 

The UI thread is the only one to access the state member variable, so no synchronization is needed. Now it's just a matter of passing control to the UI thread in such a way as to harvest the cancel output parameter of the ShowProgressDelegate. Unfortunately, our use of Control.BeginInvoke makes this complicated, because it doesn't wait for results from the UI thread. Waiting for the ShowProgress method to complete and the cancel flag to be set requires a call to the blocking Control.Invoke, but even this is a bit tricky:

 
 Sub ShowProgress(..., ByRef cancel As Boolean)   If Not Me.InvokedRequired Then       ...   Else       ' Transfer control to UI thread       Dim showProgress As ShowProgressDelegate = _           New ShowProgressDelegate(AddressOf ShowProgress)       ' Avoid boxing and losing our return value       Dim inoutCancel As Object = False       ' Show progress synchronously (so we can check for cancel)       Invoke(showProgress, New Object() {..., inoutCancel})       cancel = CBool(inoutCancel) End Sub 

It would have been nice to simply pass a Boolean variable directly to Control.Invoke to harvest the cancel parameter, but there is a problem. The problem is that a Boolean is a value type, whereas Invoke takes an array of objects, which are reference types. A value type is a simple type, such as a Boolean, that is meant to be managed on the stack. A reference type , on the other hand, comes out of the heap. Although passing a value type where a reference type is expected is certainly legal, it causes a copy of the value type (this copying is called boxing ). [2] So even though ShowProgress would be able to change the cancel flag, the change would occur on a temporary variable created by the run time on-the-fly in the heap, and we have no access to that variable.

[2] For the full treatment of value types, reference types, boxing, and unboxing, see Essential .NET (Addison-Wesley, 2003), by Don Box, with Chris Sells.

To avoid losing updates to the cancel flag, ShowProgress instead manually creates and passes a reference type variable (inoutCancel), avoiding the copy. After the synchronous call to Invoke, the code casts the object variable back to a Boolean to see whether or not the operation should be canceled.

Communication via Message Passing

The simplicity of the CalcPi example, and the resulting complexity of sending around a single Boolean indicating whether to cancel, may cause you to try a solution like the following:

 
 Sub ShowProgress(..., ByRef cancel As Boolean)   ' Make sure we're on the UI thread   If Not Me.InvokeRequired Then       ...   Else       ' Transfer control to the UI thread       Dim showProgress As ShowProgressDelegate = _           New ShowProgressDelegate(AddressOf ShowProgress)       ' Show progress synchronously (so we can check for cancel)       Invoke(showProgress, New Object() {...})       ' Check for cancel the easy, but special-purpose, way       cancel = (state = CalcState.Canceled) End Sub 

For our simple application, and others like it, that would work just fine. Because the worker thread reads only from the state field, it will always be valid (although a race condition could cause it to be old). However, don't be tempted to take this path . As soon as you have multiple outstanding requests and you keep them in an array or any kind of data structure at all, you run the risk of attempting to access data that's been invalidated (those darn race conditions again), something you'll have to protect against using synchronization (remember deadlocks?).

It's much simpler and safer to pass around ownership of the data instead of sharing access to the same data. To avoid the complexity of boxing, we recommend that you follow the delegate idiom used by the rest of .NET:

 
 Class ShowProgressArgs   Inherits EventArgs   Public Pi As String   Public TotalDigits As Integer   Public DigitsSoFar As Integer   Public Cancel As Boolean   Public Sub New(pi As String, _       totalDigits As Integer, digitsSoFar As Integer)   Me.Pi = pi   Me.TotalDigits = totalDigits   Me.DigitsSoFar = digitsSoFar   End Sub End Class Delegate Sub ShowProgressHandler(sender As Object, e As ShowProgressArgs) 

This code declares the class ShowProgressArgs, which derives from the EventArgs base class, to hold event arguments. It also declares a delegate that takes a sender and an instance on the custom arguments object. With this in place, we can use the new delegate to update ShowProgress to call itself:

 
 Sub ShowProgress(...)   ' Make sure we're on the UI thread   If Not Me.InvokeRequired Then       ...   Else       ' Transfer control to the UI thread       ' Create an instance of the delegate to call       ' the handler on the UI thread       Dim showProgress As ShowProgressHandler = _           New ShowProgressHandler(AddressOf _           AsyncCalcPiForm_ShowProgress)       ' Initialize the message parameters       Dim sender As Object = System.Threading.Thread.CurrentThread       Dim e As ShowProgressArgs = _           New ShowProgressArgs(pi, totalDigits, digitsSoFar)       ' Send the message, waiting for the UI thread to       ' return whether or not the operation should be canceled       Me.Invoke(showProgress, New Object() {sender, e})       cancel = e.Cancel End Sub ' Called on the UI thread Sub AsyncCalcPiForm_ShowProgress(sender As Object, e As ShowProgressArgs)   ' Unpack the message and forward it to the ShowProgress method   ShowProgress(e.Pi, e.TotalDigits, e.DigitsSoFar, ByRef e.Cancel) End Sub 

ShowProgress hasn't changed its signature, so CalcPi still calls it in the same simple way. However, now the worker thread will compose an instance of the ShowProgressArgs object to pass to the UI thread via a handler that looks like any other event handler, including a sender and an EventArgs-derived object. The handler calls the ShowProgress method again, breaking out the arguments from the ShowProgressArgs object. After Control.Invoke returns in the worker thread, the worker thread pulls out the cancel flag without any concern about boxing because the ShowProgressArgs type is a reference type. However, even though it is a reference type and the worker thread passes control of it to the UI thread, there's no danger of race conditions because the worker thread waits until the UI thread is finished working with the data before accessing it again.

You can further simplify this usage by updating the CalcPi method to create an instance of the ShowProgressArgs class itself, eliminating the need for an intermediate method:

 
 Sub ShowProgress(sender As Object, e As ShowProgressArgs)   ' Make sure we're on the UI thread   If Not Me.InvokeRequired Then       piTextBox.Text = e.Pi       piProgressBar.Maximum = e.TotalDigits       piProgressBar.Value = e.DigitsSoFar       ' Check for Cancel       e.Cancel = (state = CalcState.Canceled)       ' Check for completion       If e.Cancel OrElse (e.DigitsSoFar = e.TotalDigits) Then           State = CalcState.Pending           calcButton.Text = "Calc"           calcButton.Enabled = True       End If   Else       ' Transfer control to UI thread       Dim showProgress As ShowProgressHandler = _           New ShowProgressHandler(AddressOf ShowProgress)       Invoke(showProgress, New Object() { sender, e })   End If End Sub Sub CalcPi(digits As Integer)   Dim pi As StringBuilder = New StringBuilder("3", digits + 2)   Dim sender As Object = System.Threading.Thread.CurrentThread   Dim e As ShowProgressArgs = New ShowProgressArgs(pi.ToString(0,_       digits, 0)   ' Show progress (ignoring Cancel so soon)   ShowProgress(sender, e)   If digits > 0 Then       pi.Append(".")       Dim i As Integer       For i = 0 to digits  1 Step 9           Dim nineDigits As Integer = _               NineDigitsOfPi.StartingAt(i+1)           Dim digitCount As Integer = Math.Min(digits  i, 9)           Dim ds As String = String.Format("{0:D0}", nineDigits)           pi.Append(ds.Substring(0, digitCount))           ' Show progress (checking for cancel)           e.Pi = pi.ToString()           e.DigitsSoFar = i + digitCount           ShowProgress(sender, e)           If e.Cancel Then Exit For       Next   End If End Sub 

This technique represents a message passing model. This model is clear, safe, general-purpose, and scalable. It's clear because it's easy to see that the worker is creating a message, passing it to the UI, and then checking the message for information that may have been added during the UI thread's processing of the message. It's safe because the ownership of the message is never shared, starting with the worker thread, moving to the UI thread, and then returning to the worker thread, with no simultaneous access between the two threads. It's general-purpose because if the worker or UI thread needed to communicate information in addition to a Cancel flag, that information can be added to the ShowProgressArgs class. Finally, this technique is scalable because it uses a thread pool, which can handle a large number of long-running operations more efficiently than na vely creating a new thread for each one. For long-running operations in your WinForms applications, you should first consider message passing.



Windows Forms Programming in Visual Basic .NET
Windows Forms Programming in Visual Basic .NET
ISBN: 0321125193
EAN: 2147483647
Year: 2003
Pages: 139

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