Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / operating-systems / Windows

Improved Threading & UI integration under .NET 2.0

4.42/5 (23 votes)
22 Jan 20075 min read 2   552  
Improving threading and UI integration with .NET 2.0

Introduction

The need for background threading is most noticeable in the design of User Interfaces. This is certainly not a new phenomenon. Unresponsive interface issues have been plaguing users and developers alike since the inception of the GUI; at least in the Microsoft Windows arena.

There are no silver bullets which resolve the problem but framework improvements have certainly made it much easier for developers to deal with these types of issues. The tools available for dealing with non-deterministic operations have been improving with each release of the .NET framework. Release 1.1 provided some robust and diverse threading capabilities but it still didn't integrate smoothly with UI code due to thread affinity issues.

In summary, well matured and entrenched STA (Single Threaded Apartment) design constraints permeate throughout Window based controls. This means that only the thread that created the control may manipulate the control. Deviation from this constraint can and will lead to very strange behaving applications. If they behave at all!

The IDE (Integrated Development Environments) which has made the creation of User Interfaces far easier has also made it very easy for code to be developed such that it all runs on the main thread. In and of itself this isn't a problem until the UI activates a long running or non-deterministic operations such as getting data from a database or invoking a web service. All of a sudden, the user experiences slow responses from the application or worse, the dreaded, not responding message in the title bar.

With the tools currently available it is this authors opinion that only control related operations which can respond within 15 to 20 ms (guaranteed) should be executed on the main thread. All other operations should be done on alternate threads. This seemingly Draconian measure looks to be evolving more and more into standard practice. This design constraint is bolstered significantly by the framework improvements that have been coming to market such as true asynchronous ADO operations and the introduction of the Background Worker thread.

Background Worker Thread

The Background worker thread introduced in .NET 2.0 is made available on the Tool palette for Form design. This new tool component has been designed such that it aligns seamlessly with the event handling paradigm used by all the other controls.

The Background worker control is a very interesting when you consider that it has encapsulated all thread affinity issues. It makes it almost too easy to forget you are working across threads.

BackgroundWorker Object Summary

This object can be found within the toolbox for easy dropping on a form. Removes threading concerns while allowing for easy UI updating. The properties of a background worker include:

  • WorkerSupportsCancellation
  • WorkerReportsProgress
  • CancellationPending

Methods

The RunAsynch method starts the processing of the code. The work is performed on the worker thread within the DoWork event handler. The worker should check the CancellationPending attribute frequently.

The ProgressChanged and RunWorkerComplete methods both run on the UI thread. (See diagram)

UI Thread Worker Thread

Image 1 Image 2

The event handling methods added to the derived Form class look like any other methods. The intriguing and somewhat disconcerting aspect is that there isn't any indication that you are dealing with different threads. I am not saying this is a bad thing. The code is clean and succinct. It is just important to keep in the back of your mind that dead-lock, synchronization and race condition issues are factors you will need to consider when interacting between the main thread and background thread.

Basic Code Structure

C#
// this code is executed on the worker thread, called 
// from UI by RunWorkerAsync</p>

void worker_DoWork (Object sender, DoWorkEventArgs e)
{
  while (!worker.CancellationPending)
  {
    // do work
    worker.ReportProgress (percentProgress);
  }
};

// this method is called on the UI thread, invoked by 
// ReportProgress

void worker_ProgressChanged(Object sender, 
                            ProgressChangedEventArgs e)
{
  progressTextBox.Text = e.ProgressPercentage + "%";
};

// this code is called on the UI thread once the worker 
// thread terminates (completes)

void worker_RunCompleted(Object sender, 
                         RunWorkerCompletedEventArgs e)
{
  progressTextBox.Text = "Complete";
};

During debugging you can clearly see your background thread in action.

Main Thread

Image 3

Worker Thread

Image 4

Some Real World Code

I mentioned earlier that ADO has provided true asynchronous capabilities. In prior incarnations of ADO, asynchronous operations were available but at the cost of spawning and blocking on a synchronous thread. ADO 2.0 doesn't work that way. It is truly asynchronous.

I decided to blend the ADO feature together with the background thread and the binding ability of Generics to controls. The results were outstanding. I decided to select a long running operation that would significantly disrupt the UI if executed on the main thread. The results yield a very responsive GUI. Please bear in mind the operation isn't any faster but the perception of usability greatly enhances the application. Depending upon the application, it might actually let the user do something else while she waits for the data.

The DoWork event handler shows the invocation of a length database stored procedure call. The points to take away are that the code checks for a user initiated cancellation and post progress messages frequently.

C#
private void backgroundWorker1_DoWork(object sender, 
                                      DoWorkEventArgs e)
{
  Object state;

  BackgroundWorker bw = sender asBackgroundWorker;

  int percentProgress = 0;

  // atomic operation
  Interlocked.Exchange(ref percentProgress, tickCounter); 

  string cnString = "Integrated Security=SSPI;
                    Persist Security Info=True;
                    Initial Catalog=pdm_snapshots;
                    DataSource=pm-db-dev,10501;
                    Asynchronous Processing=true";
  
  state = (string)"Running query...";

  using (SqlConnection cn = newSqlConnection(cnString))
  {
    try
    {
      string proc = "spGetGroup";
     
      SqlCommand cmd = newSqlCommand(proc, cn);
      
      cn.Open();
      
      IAsyncResult result = cmd.BeginExecuteReader();
      
      while ((!result.IsCompleted) &&
              (!bw.CancellationPending))
      {
        percentProgress = tickCounter;

        bw.ReportProgress(percentProgress, state);
        
        Thread.Sleep(5);   // do not monopolize resource. 
                           // relinquish some time
      }
      
      if (bw.CancellationPending)
      {
        // exit when you reach max running time
        
        cmd.Cancel();
        e.Cancel = true;
        
        return;
      }
      
      // At this point we have a result
      // atomic operation

      percentProgress = Interlocked.Exchange(
                                ref tickCounter, 0); 
      
      state = (string)"Reading...";
      
      using (SqlDataReader rdr = cmd.EndExecuteReader(
                                                  result))
      {
        while (rdr.Read() &&
                (!bw.CancellationPending))
        {
          // load collection
          data.Add((int)rdr.GetValue(0));
          
          percentProgress = tickCounter;
          bw.ReportProgress(percentProgress, state);

          Thread.Sleep(5);   // do not monopolize resource.
                             // relinquish some time
        }

      if (bw.CancellationPending)
          e.Cancel = true;
        
      if (!rdr.IsClosed)
          rdr.Close();
    }
  }
  catch (InvalidOperationException ex)
  {
    throw ex;
  }     
  catch (SqlException ex)
  {
    throw new InvalidOperationException(ex.Message);
  }
}

Upon conclusion of the worker thread, the Run worker completed event is called on the main thread. The interesting bits to take away are that thrown events from the background worker are caught and may be tested through the RunWorkerCompletedEventArgs object.

The code also depicts a Generic (List<int>) bind to a control. The Generic serves as the shared memory construct operated upon by both the worker thread and the main Ui thread.

C#
private void backgroundWorker1_RunWorkerCompleted(object
                    sender, RunWorkerCompletedEventArgs e)
{
  // Run on UI thread
  // raised by the worker on cancellation, completion.

  if (e.Error != null)
  {
    lblPercent.Text = "Error encountered :
                      " + e.Error.Message;
  }
  else
    if (e.Cancelled)
    {
      lblPercent.Text = "Operation cancelled!";
    }

  timer1.Enabled = false;

  Interlocked.Exchange(ref tickCounter, 0);

  progressBar1.Value = 0;
  lstData.UseWaitCursor = false;

  btnGetData.Enabled = true;

  // make final progress update 

  BindingSource bs = newBindingSource();
  bs.DataSource = data;
  lstData.DataSource = bs;
}

Conclusion

As we accelerate into the realm of multi-cores and exceedingly more complex and loosely coupled systems, the need for easy to use threading functions is ever increasing. The .NET framework is evolving to meet those and other challenges. The background worker thread is but one small example of a productivity enhancement which can be leveraged to provide the best user experiences for our customers.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here