Safe, Simple Multithreading in Windows Forms, Part 2

 

Chris Sells

September 2, 2002

Summary: Explores how to leverage multiple threads to split the user interface (UI) from a long-running operation while communicating further user input to the worker thread to adjust its behavior, thus allowing a message-passing scheme for robust, correct multithreaded processing. (8 printed pages)

Download the asynchcaclpi.exe sample file.

As you may recall from a couple of columns ago, Safe, Simple Multithreading in Windows Forms, Part 1, Windows Forms and threading can be used together with good results if you're careful. Threading is a nice way to perform long-running operations, like calculating pi to a large number of digits as shown in Figure 1 below.

Figure 1. Digits of Pi application

Windows Forms and Background Processing

In the last article, we explored starting threads directly for background processing, but settled on using asynchronous delegates to get our worker thread started. Asynchronous delegates have the convenience of syntax when passing arguments and scale better by taking threads from a process-wide, common language runtime-managed pool. The only real problem we ran into was when the worker thread wanted to notify the user of progress. In this case, it wasn't allowed to work with the UI controls directly (a long-standing Win32® UI no-no). Instead, the worker thread has to send or post a message to the UI thread, using Control.Invoke or Control.BeginInvoke to cause code to execute on the thread that owns the controls. These considerations resulted in the following code:

// Delegate to begin asynch calculation of pi
delegate void CalcPiDelegate(int digits);
void _calcButton_Click(object sender, EventArgs e) {
  // Begin asynch calculation of pi
  CalcPiDelegate calcPi = new CalcPiDelegate(CalcPi);
  calcPi.BeginInvoke((int)_digits.Value, null, null);
}

void CalcPi(int digits) {
  StringBuilder pi = new StringBuilder("3", digits + 2);

  // Show progress
  ShowProgress(pi.ToString(), digits, 0);

  if( digits > 0 ) {
    pi.Append(".");

    for( int i = 0; i < digits; i += 9 ) {
      ...
      // Show progress
      ShowProgress(pi.ToString(), digits, i + digitCount);
    }
  }
}

// Delegate to notify UI thread of worker thread progress
delegate
void ShowProgressDelegate(string pi, int totalDigits, int digitsSoFar);

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {
  // Make sure we're on the right thread
  if( _pi.InvokeRequired == false ) {
    _pi.Text = pi;
    _piProgress.Maximum = totalDigits;
    _piProgress.Value = digitsSoFar;
  }
  else {
    // Show progress synchronously
    ShowProgressDelegate showProgress =
      new ShowProgressDelegate(ShowProgress);
    this.BeginInvoke(showProgress,
      new object[] { pi, totalDigits, digitsSoFar });
  }
}

Notice that we have two delegates. The first, CalcPiDelegate, is for use in bundling up the arguments to be passed to CalcPi on the worker thread allocated from the thread pool. An instance of this delegate is created in the event handler when the user decides they wants to calculate pi. The work is queued to the thread pool by calling BeginInvoke. This first delegate is really for the UI thread to pass a message to the worker thread.

The second delegate, ShowProgressDelegate, is for use by the worker thread that wants to pass a message back to the UI thread, specifically an update on how things are progressing in the long-running operation. To shield callers from the details of thread-safe communication with the UI thread, the ShowProgress method uses the ShowProgressDelegate to send a message to itself on the UI thread through the Control.BeginInvoke method. Control.BeginInvoke asynchronously queues work up for the UI thread and then continues on without waiting for the result.

Canceling

In this example, we're able to 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 it progress as it goes. Likewise, the worker thread doesn't have to wait for the UI thread to show progress so 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, the user would still like the option to cancel the calculation if they've decided they need 1,000,001 digits and they mistakenly asked for only 1,000,000. An updated UI for CalcPi that allows for cancellation is shown in Figure 2.

Figure 2. Letting the user cancel a long-running operation

Implementing cancel for a long running operation is a multi-step process. First, a UI needs to be provided for the user. In this case, the Calc button was changed to a Cancel button after a calculation has begun. Another popular choice is a progress dialog, which typically includes current progress details, including a progress bar showing percentage of work complete, as well as a Cancel button.

If the user decides to cancel, that should be noted in a member variable and the UI disabled for the small amount of time between when the UI thread knows the worker thread should stop, but before the worker thread itself knows and has a chance to stop sending progress. If this period of time is ignored, it's possible that 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 that's supposed to be shutting down. While it's certainly possible to 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), it's often simpler to pause the UI for the brief amount of time between when the UI knows the worker thread is going to stop, but before the worker thread knows. Implementing it for our simple pi calculator is a matter of keeping a tri-value enum, as shown here:

enum CalcState {
    Pending,     // No calculation running or canceling
    Calculating, // Calculation in progress
    Canceled,    // Calculation canceled in UI but not worker
}

CalcState _state = CalcState.Pending;

Now, depending on what state we're in, we treat the Calc button differently, as shown here:

void _calcButton_Click(...)  {
    // Calc button does double duty as Cancel button
    switch( _state ) {
        // Start a new calculation
        case CalcState.Pending:
            // Allow canceling
            _state = CalcState.Calculating;
            _calcButton.Text = "Cancel";

            // Asynch delegate method
            CalcPiDelegate  calcPi = new CalcPiDelegate(CalcPi);
            calcPi.BeginInvoke((int)_digits.Value, null, null);
            break;

        // Cancel a running calculation
        case CalcState.Calculating:
            _state = CalcState.Canceled;
            _calcButton.Enabled = false;
            break;

        // Shouldn't be able to press Calc button while it's canceling
        case CalcState.Canceled:
            Debug.Assert(false);
            break;
    }
}

Notice that when the Calc/Cancel button is pressed in Pending state, we send the state to Calculating (as well as changing the label on the button), and start the calculation asynchronously as we did before. If the state is Calculating when the Calc/Cancel button is pressed, we switch the state to Canceled and disable the UI to start a new calculation for as long as it takes us to communicate the canceled stated to the worker thread. Once we've communicated to the worker thread that the operation should be canceled, we'll enable the UI again and reset the state back to Pending so that the user can begin another operation. To communicate to the worker that it should cancel, let's augment the ShowProgress method to include a new out parameter:

void ShowProgress(..., out bool cancel)

void CalcPi(int digits) {
    bool cancel = false;
    ...

    for( int i = 0; i < digits; i += 9 ) {
        ...

        // Show progress (checking for Cancel)
        ShowProgress(..., out cancel);
        if( cancel ) break;
    }
}

You may be tempted to make the cancel indicator a Boolean return value from ShowProgress, but I can never remember if true means to cancel or that everything is fine/continue as normal, so I use the out parameter technique, which even I can keep straight.

The only thing left to do is updating the ShowProgress method, which is the code that actually performs the transition between worker thread and UI thread, to notice if the user has asked to cancel and to let CalcPi know accordingly. Exactly how we communicate that information between the UI and the worker thread depends on which technique we'd like to use.

Communication with Shared Data

The obvious way to communicate the current state of the UI is to let the worker thread access the _state member variable directly. We could accomplish this with the following code:

void ShowProgress(..., out bool cancel) {
  // Don't do this!
  if( _state == CalcState.Cancel ) {
    _state = CalcState.Pending;
    cancel = true;
  }
  ...
}

I hope that something inside you cringed when you saw this code (and not just because of the warning comment). If you're going to be doing multithreaded programming, you're going to have to watch out any time that two threads can have simultaneous access to the same data (in this case, the _state member variable). Shared access to data between threads makes it easy to get into race conditions where 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 need to monitor usage of your shared data to make sure that each thread waits patiently while the other thread works on the data. To monitor access to shared data, .NET provides the Monitor class to be used on a shared object to act as the lock on the data, which C# wraps with the handy lock block:

object _stateLock = new object();

void ShowProgress(..., out bool cancel) {
  // Don't do this either!
  lock( _stateLock ) { // Monitor the lock
    if( _state == CalcState.Cancel ) {
      _state = CalcState.Pending;
      cancel = true;
    }
    ...
  }
}

Now I've properly locked access to the shared data, but I've done it in such a way as to make it very likely to cause another common problem when performing multithreaded programming—deadlock. When two threads are deadlocked, both of them wait for the other to complete their work before continuing, making sure that neither actually progresses.

If all this talk of race conditions and deadlocks has caused you concern—good. Multithreaded programming with shared data is darn hard. So far we've been able to avoid these issues because we have been passing around copies of data of which each thread gets complete ownership. Without shared data, there's no need for synchronization. If you find that you need access to shared data (that is, the overhead of copying the data is too great a space or time burden), then you'll need to study up on sharing data between threads (check the References section for my favorite study aid in this area).

However, the vast majority of multithreading scenarios, especially as related to UI multithreading, seem to work best with the simple message-passing scheme we've been using so far. Most of the time, you don't want the UI to have access to data being worked on in the background (the document being printed or the collection of objects being enumerated, for example). For these cases, avoiding shared data is the best choice.

Communicating with Method Parameters

We've already augmented our ShowProgress method to contain an out parameter. Why not let ShowProgress check the state of the _state variable when it's executing on the UI thread, like so:

void ShowProgress(..., out bool cancel) {
    // Make sure we're on the UI thread
    if( _pi.InvokeRequired == false ) {
        ...

        // Check for Cancel
        cancel = (_state == CalcState.Canceled);

        // Check for completion
        if( cancel || (digitsSoFar == totalDigits) ) {
            _state = CalcState.Pending;
            _calcButton.Text = "Calc";
            _calcButton.Enabled = true;

        }
    }
    // Transfer control to UI thread
    else { ... }
}

Because the UI thread is the only one accessing the _state member variable, 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 out parameter of the ShowProgressDelegate. Unfortunately, our use of Control.BeginInvoke makes this complicated. The problem is that BeginInvoke won't wait around for a result of calling ShowProgress on the UI thread, so we have two choices. One option is to pass another delegate to BeginInvoke to be called when ShowProgress has returned from the UI thread, but that will happen on another thread in the thread pool, so we'll have to go back to synchronization, this time between the worker thread another thread from the pool. A simpler option is to switch to the synchronous Control.Invoke method and wait for the cancel out parameter. However, even this is a bit tricky as you can see in the following code:

void ShowProgress(..., out bool cancel) {
    if( _pi.InvokeRequired == false ) { ... }
    // Transfer control to UI thread
    else {
        ShowProgressDelegate  showProgress =
            new ShowProgressDelegate(ShowProgress);

        // Avoid boxing and losing our return value
        object inoutCancel = false;

        // Show progress synchronously (so we can check for cancel)
        Invoke(showProgress, new object[] { ..., inoutCancel});
        cancel = (bool)inoutCancel;
    }
}

While it would have been ideal to simply pass a Boolean variable directly to Control.Invoke to harvest the cancel parameter, we have a problem. The problem is that bool is a value data type, whereas Invoke takes an array of objects as parameters, and objects are reference data types. Check the References section for books discussing the difference, but the upshot is that a bool passed as an object will be copied, leaving our actual bool unchanged, meaning we'd never know when the operation was canceled. To avoid this situation, we create our own object variable (inoutCancel) and pass it instead, avoiding the copy. After the synchronous call to Invoke, we cast the object variable to a bool to see if the operation should be canceled or not.

The value versus reference type distinction is something you'll have to watch out for whenever you call Control.Invoke (or Control.BeginInvoke) with out or ref parameters that are value types, such as primitive types like int or bool, as well as enums and structs. However, if you've got more complicated data being passed around as a custom reference type aka class, you don't need to do anything special to make things work. However, even the unpleasantness of handling value types with Invoke/BeginInvoke pales in comparison to getting multithreaded code to access shared data in a race condition/deadlock-free way, so I consider it a small price to pay.

Conclusion

Once again, we've used a seemingly trivial example to explore some complicated issues. Not only have we leveraged multiple threads to split the UI from a long-running operation, but also we've communicated further user input back to the worker thread to adjust its behavior. While we could have used shared data, to avoid the complications of synchronization (which only arise when your boss tries your code), we've stayed with our message-passing scheme for robust, correct multithreaded processing.

References