Export (0) Print
Expand All

Chapter 6 — Using Multiple Threads

 

patterns & practices Developer Center

Smart Client Architecture and Design Guide

David Hill, Brenton Webster, Edward A. Jezierski, Srinath Vasireddy and Mohammad Al-Sabt, Microsoft Corporation; Blaine Wastell, Ascentium Corporation; Jonathan Rasmusson and Paul Gale, ThoughtWorks; and Paul Slater, Wadeware LLC

June 2004

Related Links

Microsoft® patterns & practices Library http://msdn.microsoft.com/en-us/practices/default.aspx

Application Architecture for .NET: Designing Applications and Services http://msdn.microsoft.com/en-us/library/ms954595.aspx

Summary: This chapter discusses the issues surrounding the use of multiple threads in a smart client application. To maximize the responsiveness of your smart client applications, you need to carefully consider how and when to use multiple threads. Threads can significantly improve the usability and performance of your application, but they require very careful consideration when you determine how they will interact with the user interface.

Contents

Multithreading in the .NET Framework
When to Use Multiple Threads
Creating and Using Threads
Using Tasks to Handle Interaction Between the UI Thread and Other Threads
Summary

A thread is a basic unit of execution. A single thread executes a series of application instructions, following a single path of logic through the application. All applications have at least one thread, but you can design your applications so that they use multiple threads, with each thread executing separate logic. By using multiple threads in your application, you can process lengthy or time-consuming tasks in the background. Even on a computer with a single processor, the use of multiple threads can significantly improve the responsiveness and usability of your application.

Developing your application to use multiple threads can be very complicated, particularly if you do not carefully consider locking and synchronization issues. As you develop your smart client application, you need to carefully evaluate where and how multiple threads should be used so that you can gain maximum advantage without creating applications that are unnecessarily complex and difficult to debug.

This chapter examines some of the concepts that are most important for developing multithreaded smart client applications. It looks at some of the recommended uses for multiple threads in a smart client application, and it describes how to implement these capabilities.

Multithreading in the .NET Framework

All .NET Framework applications are created with a single thread, which is used to execute the application. In smart client applications, this thread creates and manages the user interface (UI) and is called the UI thread.

You can use the UI thread for all processing, including Web service calls, remote object calls, and calls into a database. However, using the UI thread in this way is generally not a good idea. In most cases, you will be unable to predict how long a call to a Web service, remote object, or database will take, and you may cause the UI to freeze while the UI thread waits for a response.

reating additional threads enables your application to perform additional processing without using the UI thread. You can use multiple threads to prevent the UI from freezing while the application makes a Web service call, or to perform certain local tasks in parallel to increase the overall efficiency of your application. In most cases, you should strongly consider performing any tasks not related to the UI on a separate thread.

Choosing Between Synchronous and Asynchronous Calls

Applications can make both synchronous and asynchronous calls. A synchronous call waits for a response or return value before proceeding. A call is said to be blocked if it is not allowed to proceed.

An asynchronous, or nonblocking call, does not wait for a response. Asynchronous calls are carried out by using a separate thread. The original thread initiates the asynchronous call, which uses another thread to carry out the request while the original thread continues processing.

With smart client applications, it is important to minimize synchronous calls from the UI thread. As you design your smart client application, you should consider each call your application will make and determine whether a synchronous call may negatively affect the application's responsiveness and performance.

Use synchronous calls from the UI thread only when:

  • Performing operations that manipulate the UI.
  • Performing small, well-defined operations that pose no risk of causing the UI to freeze.

Use asynchronous calls from the UI thread when:

  • Performing background operations that do not affect the UI.
  • Making calls into other systems or resources located on the network.
  • Performing operations that may take a long time to complete.

Choosing Between Foreground and Background Threads

All threads in the .NET Framework are designated as foreground threads or background threads. The two have only one difference — background threads do not prevent a process from terminating. After all foreground threads belonging to a process have terminated, the common language runtime (CLR) ends the process, terminating any background threads that are still running.

By default, all threads generated by creating and starting a new Thread object are foreground threads, and all threads that enter the managed execution environment from unmanaged code are marked as background threads. However, you can modify whether a thread is a foreground or background thread by modifying the Thread.IsBackground property. A thread is designated as a background thread by setting Thread.IsBackground to true, and is designated a foreground thread by setting Thread.IsBackground to false.

Note   For more information about the Thread object, see "Using the Thread Class" later in this chapter.

In most applications, you will choose to set different threads as either foreground or background threads. Usually, you should set threads that passively listen for an activity as background threads, and set threads responsible for sending data as foreground threads so that the thread is not terminated before all the data is sent.

You should use background threads only when you are sure that there will be no adverse effects of the thread being unceremoniously terminated by the system. Use a foreground thread when the thread is performing sensitive or transactional operations that need to be completed, or when you need to control how the thread is shut down so that important resources can be released.

Handling Locking and Synchronization

Sometimes when you build applications, you create multiple threads that all need to use key resources, such as data or application components, at the same time. If you are not careful, one thread could make a change to a resource while another thread is working with it. The result may be that the resource is left in an indeterminate state and is rendered unusable. This is known as a race condition. Other adverse effects of using multiple threads without carefully considering shared resource usage include deadlocks, thread starvation, and thread affinity issues.

To prevent these effects when accessing a resource from two or more threads, you need to coordinate the threads that are trying to access the resource by using locking and synchronization techniques.

Managing thread access to shared resources using locking and synchronization is a complex task and should be avoided wherever possible by passing data between threads rather than providing shared access to a single instance.

If you can't eliminate resource sharing between threads, you should:

  • Use the lock statement in Microsoft Visual C#® and the SyncLock statement in Microsoft® Visual Basic® .NET to create a critical section, but beware of making method calls from within a critical section to prevent deadlocks.
  • Use the Synchronized method to obtain thread-safe .NET collections.
  • Use the ThreadStatic attribute to create per-thread members.
  • Use a double-check lock or the Interlocked.CompareExchange method to prevent unnecessary locking.
  • Ensure that static state is thread safe.

For more information about locking and synchronization techniques, see "Threading Design Guidelines" in .NET Framework General Reference at http://msdn.microsoft.com/en-us/library/f857xew0(VS.71).aspx.

Using Timers

In some situations, you may not need to use a separate thread. If your application needs to perform simple, UI-related operations periodically, you should consider using a process timer. Process timers are sometimes used in smart client applications to:

  • Perform operations at regularly scheduled times.
  • Maintain consistent animation speeds (regardless of processor speed) when working with graphics.
  • Monitor servers and other applications to confirm that they are online and running.

The .NET Framework provides three process timers:

  • System.Window.Forms.Timer
  • System.Timers.Timer
  • System.Threading.Timer

System.Window.Forms.Timer is useful if you want to raise events in a Windows Forms application. It is specifically optimized to work with Windows Forms and must be used within a Windows Form. It is designed to work in a single-threaded environment and operates synchronously on the UI thread. This means that this timer will never preempt the execution of application code (assuming that you do not call Application.DoEvents) and is safe to interact with the UI.

System.Timers.Timer is designed and optimized for use in multithreaded environments. Unlike System.Window.Forms.Timer, this timer calls your event handler on a worker thread obtained from the CLR thread pool. You should ensure that the event handler does not interact with the UI in this case. System.Timers.Timer exposes a SynchronizingObject property that can mimic behavior from System.Windows.Forms.Timer, but unless you need more precise control over the timing of the events, you should use System.Windows.Forms.Timer instead.

System.Threading.Timer is a simple, lightweight server-side timer. It is not inherently thread safe and it is more cumbersome to use than other timers. This timer is generally not suitable for Windows Forms environments.

Table 6.1 lists the various properties of each timer.

Table 6.1   Process Timer Properties

PropertySystem.Windows.FormsSystem.TimersSystem.Threading
Timer event runs on what thread?UI threadUI or worker threadWorker thread
Instances are thread safe?NoYesNo
Requires Windows Forms?YesNoNo
Initial timer event can be scheduled?NoNoYes

When to Use Multiple Threads

Multithreading can be used in many common situations to significantly improve the responsiveness and usability of your application.

You should strongly consider using multiple threads to:

  • Communicate over a network, for example to a Web server, database, or remote object.
  • Perform time-consuming local operations that would cause the UI to freeze.
  • Distinguish tasks of varying priority.
  • Improve the performance of application startup and initialization.

It is useful to examine these uses in more detail.

Communicating Over a Network

Smart-clients may communicate over a network in a number of ways, including:

  • Remote object calls, such as DCOM, RPC or .NET remoting.
  • Message-based communications, such as Web service calls and HTTP requests.
  • Distributed transactions.

Many factors determine how fast a network service responds to an application making a request, including the nature of the request, network latency, reliability and bandwidth of a connection, and how busy the service or services are.

This unpredictability can cause problems with the responsiveness of single-threaded applications, and multithreading is often a good solution. You should create a separate thread to the UI thread for all communication over a network, and then pass the data back to the UI thread when a response is received.

It is not always necessary to create separate threads for network communication. If your application communicates over the network asynchronously, for example using Microsoft Windows Message Queuing (also known as MSMQ), it does not wait for a response before continuing. However, even in this case, you should still use a separate thread to listen for and process the response when it arrives.

Performing Local Operations

Even in situations where processing occurs locally, some operations may take enough time to negatively affect the responsiveness of your application. Such operations include:

  • Image rendering.
  • Data manipulation.
  • Data sorting.
  • Searching.

You should not perform operations such as these on the UI thread because doing so causes performance problems in your application. Instead, you should use an additional thread to perform these operations asynchronously and prevent the UI thread from blocking.

In many cases, you should also design the application so that it reports the progress and success or failure of ongoing background operations. You may also consider allowing the user to cancel background operations to improve usability.

Distinguishing Tasks of Varying Priority

Not all of the tasks your application has to perform will be of the same priority. Some tasks will be time critical, and others will not. In other cases, you may find that one thread is dependent on the results of processing on another thread.

You should create threads of different priorities to reflect the priorities of the tasks they are performing. For example, you should use a high-priority thread to manage time-critical tasks, and a low-priority thread to perform passive tasks or tasks that are not time-sensitive.

Application Startup

Your application often has to perform a number of operations when it first runs. For example, it may need to initialize its state, retrieve or update data, and open connections to local resources. You should consider using a separate thread to initialize your application, allowing the user to start using the application as soon as possible. Using a separate thread for initialization increases your application's responsiveness and usability.

If you do perform initialization on a separate thread, you should prevent the user from initiating operations that depend on initialization being completed, by updating the UI menu and toolbar button state after initialization is complete. You should also provide clear feedback that notifies users of the initialization progress.

Creating and Using Threads

There are several ways that you can create and use background threads in the .NET Framework. You can use the ThreadPool class to access the pool of threads managed by the .NET Framework for a given process, or you can use the Thread class to explicitly create and manage a thread. Alternatively, you can use delegate objects or a Web service proxy to cause specific processing to occur on a non–UI thread. This section examines each of these different methods in turn and makes recommendations about when each should be used.

Using the ThreadPool Class

By now you probably realize that many of your applications would benefit from multithreading. However, thread management is not just a question of creating a new thread each time you want to perform a different task. Having too many threads can cause an application to use an unnecessary number of system resources, particularly if you have a large number of short-running operations, all of which are running on separate threads. Also, managing a large number of threads explicitly can be very complex.

Thread pooling solves these problems by providing your application with a pool of worker threads that are managed by the system, allowing you to concentrate on application tasks rather than thread management.

Threads can be added to the thread pool as required by the application. When the CLR initially starts, the thread pool contains no additional threads. However, as your application requests threads, they are dynamically created and stored in the pool. If threads are not used for some time, they can be disposed of, so the thread pool shrinks and grows according to the demands of the application.

Note   One thread pool is created per process, so if you run several application domains within the same process, an error in one application domain can affect the rest within the same process because they use the same thread pool.

A thread pool consists of two types of threads:

  • Worker threads. The worker threads are part of the standard system pool. They are standard threads managed by the .NET Framework, and most functions are executed on them.
  • Completion port threads. This kind of thread is used for asynchronous I/O operations, using the IOCompletionPorts API.
Note   If the application is trying to perform I/O operations with a computer that does not have IOCompletionPorts functionality, it will revert to using worker threads.

The thread pool contains a default of 25 threads per computer processor. If all 25 threads are being used, additional requests queue until a thread becomes available. Each thread uses the default stack size and runs at the default priority.

The following code example shows the use of a thread pool.

private void ThreadPoolExample()
{
    WaitCallback callback = new WaitCallback( ThreadProc );
    ThreadPool.QueueUserWorkItem( callback );
}

In the preceding code, you first create a delegate to reference the code you want executed on a worker thread. The .NET Framework defines the WaitCallback delegate, which references a method that takes a single object parameter and returns no values. The following method implements the code you want executed.

private void ThreadProc( Object stateInfo )
{
    // Do something on worker thread.
}

You can pass a single object argument to the ThreadProc method by specifying it as the second parameter in the QueueUserWorkItem method call. In the preceding example, no arguments are passed to the ThreadProc method, so the stateInfo parameter will be null.

Use the ThreadPool class when:

  • You have a large number of small and independent tasks that are to be performed in the background.
  • You do not need to have fine control over the thread used to perform a task.

Using the Thread Class

You can explicitly manage threads by using the Thread class. This includes threads created by the CLR and those created outside the CLR that enter the managed environment to execute code. The CLR monitors all of the threads in its process that have ever executed code within the .NET Framework and uses an instance of the Thread class to manage them.

Whenever you can, you should create threads using the ThreadPool class. However, there are several situations where you will need to create and manage your own threads instead of using the ThreadPool class.

Use a Thread object when:

  • You need a task to have a particular priority.
  • You have a task that might run a long time (and therefore might block other tasks).
  • You need to ensure that particular assemblies can be accessed by only one thread.
  • You need to have a stable identity associated with the thread.

The Thread object contains a number of properties and methods that help you control threads. You can set the priority of thread, query the current thread state, abort threads, temporarily block threads, and perform many other thread management tasks.

The following code example demonstrates the use of the Thread object to create and start a thread.

static void Main() 
{
    System.Threading.Thread workerThread =
        new System.Threading.Thread( SomeDelegate );
    workerThread.Start();
}
public static void SomeDelegate () { Console.WriteLine( "Do some work." ); }

In this example, SomeDelegate is a ThreadStart delegate — a reference to the code that will be executed on the new thread. Thread.Start submits a request to the operating system to start the thread.

If you instantiate a new thread this way, you cannot pass any arguments to the ThreadStart delegate. If you need to pass an argument to a method to be executed on another thread, you should create a custom delegate with the required method signature and invoke it asynchronously.

For more information about custom delegates, see "Using Delegates" later in this chapter.

If you need to receive updates or results from a separate thread, you can use a callback method — a delegate that references code to be called after the thread finishes its work — that allows threads to interact with the UI. For more information, see "Using Tasks to Handle Interactions Between the UI Thread and Other Threads" later in this chapter.

Using Delegates

A delegate is a reference (or a pointer) to a method. When you define a delegate, you specify the exact method signature that other methods must match if they want to represent the delegate. All delegates can be invoked both synchronously and asynchronously.

The following code example shows how to declare a delegate. This example shows a long-running calculation implemented as a method in a class.

delegate string LongCalculationDelegate( int count );

If the .NET Framework encounters a delegate declaration like the previous one, it implicitly declares a hidden class derived from the MultiCastDelegate class, as shown in the following code example.

Class LongCalculationDelegate : MutlicastDelegate
{
    public string Invoke( count );
    public void BeginInvoke( int count, AsyncCallback callback,
        object asyncState );
    public string EndInvoke( IAsyncResult result );
}

The delegate type LongCalculationDelegate is used to reference a method that takes a single integer parameter and returns a string. The following code example instantiates a delegate of this type that references a specific method with the relevant signature.

LongCalculationDelegate longCalcDelegate =
            new LongCalculationDelegate( calculationMethod );

In the example, calculationMethod is the name of a method that implements the calculation you want performed on a separate thread.

You can invoke the method referenced by the delegate instance either synchronously or asynchronously. To invoke it synchronously, use the following code.

string result = longCalcDelegate( 10000 );

This code internally uses the Invoke method defined in the delegate type above. Because the Invoke method is a synchronous call, this method returns only after the invoked method returns. The return value is the result of the invoked method.

More frequently, to prevent the calling thread from blocking, you will choose to invoke the delegate asynchronously, using the BeginInvoke and EndInvoke methods. Asynchronous delegates use the thread pooling capabilities of the .NET Framework for thread management. The standard Asynchronous Call pattern implemented by the .NET Framework provides the BeginInvoke method to initiate the required operation on a thread, and it provides the EndInvoke method to allow the asynchronous operation to be completed and any resulting data to be passed back to the calling thread. After the background processing completes, you can invoke a callback method within which you can call EndInvoke to retrieve the result of the asynchronous operation.

When you call the BeginInvoke method, it does not wait for the call to complete; instead, it immediately returns an IAsyncResult object, which can be used to monitor the progress of the call. You can use the WaitHandle member of the IAsyncResult object to wait for the asynchronous call to complete or use the IsComplete member to poll for completion. If you call the EndInvoke method before the call completes, it will block and return only after the call completes. However, you should be careful not to use these techniques to wait for the call to complete, because they may block the UI thread. In general, the callback mechanism is the best way to be notified that the call has completed.

To execute a method referenced by a delegate asynchronously

  1. Define a delegate representing the long-running asynchronous operation, as shown in the following example.
    delegate string LongCalculationDelegate( int count );
    
  2. Define a method matching the delegate signature. The following example method simulates a time-consuming operation by causing the thread to sleep for count milliseconds before returning.
    private string LongCalculation( int count )
    {
        Thread.Sleep( count );
        return count.ToString();
    }
    
  3. Define a callback method that corresponds to the AsyncCallback delegate defined by the .NET Framework, as shown in the following example.
    private void CallbackMethod( IAsyncResult ar )
    {
        // Retrieve the invoking delegate.
        LongCalculationDelegate dlgt = (LongCalculationDelegate)ar.AsyncState;
        // Call EndInvoke to retrieve the results.
        string results = dlgt.EndInvoke(ar);
    }
    
  4. Create an instance of a delegate that references the method you want to call asynchronously and create an AsyncCallback delegate that references the callback method, as shown in the following code example.
        LongCalculationDelegate longCalcDelegate =
                new LongCalculationDelegate( calculationMethod );
        AsyncCallback callback = new AsyncCallback( CallbackMethod );
        
    
  5. From your calling thread, initiate the asynchronous call by calling the BeginInvoke method on the delegate that references the code you want to execute asynchronously.
        longCalcDelegate.BeginInvoke( count, callback, longCalcDelegate );
    

    The method LongCalculation is called on the worker thread. When it completes, the method CallbackMethod is called, and the results of the calculation retrieved.

Note   The callback method is executed on a non-UI thread. To modify the UI, you need to use techniques to switch from this thread to the UI thread. For more information, see "Using Tasks to Handle Interactions Between the UI Thread and Other Threads" later in this chapter.

You can use a custom delegate to pass arbitrary parameters to a method to be executed on a separate thread (something you cannot do when you create threads directly using either the Thread object or a thread pool.)

Invoking delegates asynchronously is particularly useful when you need to invoke long-running operations in the application UI. If users perform an operation in the UI that is expected to take a long time to complete, you do not want the UI to freeze and not be able to refresh itself. Using an asynchronous delegate, you can return control to your main UI thread to perform other operations.

You should use a delegate to invoke a method asynchronously when:

  • You need to pass arbitrary parameters to a method you want to execute asynchronously.
  • You want to use the Asynchronous Call pattern provided by the .NET Framework.
Note   For more details about how to use BeginInvoke and EndInvoke to make asynchronous calls, see "Asynchronous Programming Overview" in the .NET Framework Developer's Guide at http://msdn.microsoft.com/en-us/library/ms228963.aspx.

Calling Web Services Asynchronously

Applications often communicate with network resources using Web services. In general, you should not call a Web service synchronously from the UI thread, because response times to Web service calls vary widely, as do response times in all interactions over the network. Instead, you should call all Web services asynchronously from the client.

To see how to call Web services asynchronously, consider the following simple Web service, which sleeps for a period of time and then returns a string indicating that it has completed its operation.

[WebMethod]
public string ReturnMessageAfterDelay( int delay )
{
    System.Threading.Thread.Sleep(delay);
    return "Message Received";
}

When you reference a Web service in the Microsoft Visual Studio® .NET development system, it automatically generates a proxy. A proxy is a class that allows your Web services to be invoked asynchronously using the Asynchronous Call pattern implemented by the .NET Framework. If you examine the proxy that is generated, you will see the following three methods.

public string ReturnMessageAfterDelay( int delay )
{
    object[] results = this.Invoke( "ReturnMessageAfterDelay",
                                    new object[] {delay} );
    return ((string)(results[0]));
}
public System.IAsyncResult BeginReturnMessageAfterDelay( int delay,
                          System.AsyncCallback callback, object asyncState )
{
    return this.BeginInvoke( "ReturnMessageAfterDelay",
                             new object[] {delay}, callback, asyncState );
}
public string EndReturnMessageAfterDelay( System.IAsyncResult asyncResult )
{
      object[] results = this.EndInvoke( asyncResult );
      return ((string)(results[0]));
}

The first method is the synchronous method for invoking the Web service. The second and third methods are asynchronous methods. You can call the Web service asynchronously as follows.

private void CallWebService()
{
    localhost.LongRunningService serviceProxy =
                    new localhost.LongRunningService();
    AsyncCallback callback = new AsyncCallback( Completed );
    serviceProxy.BeginReturnMessageAfterDelay( callback, serviceProxy, null );
}

This example is very similar to the asynchronous callback example using a custom delegate. You define an AsyncCallback object with a method that will be invoked when the Web service returns. You invoke the asynchronous Web service with a method that specifies the callback and the proxy itself, as shown in the following code example.

void Completed( IAsyncResult ar )
{
    localhost.LongRunningService serviceProxy =
        (localhost.LongRunningService)ar.AsyncState;
    string message = serviceProxy.EndReturnMessageAfterDelay( ar );
}

When the Web service completes, the completed callback method is called. You can then retrieve your asynchronous result by calling EndReturnMessageAfterDelay on the proxy.

Using Tasks to Handle Interaction Between the UI Thread and Other Threads

One of the most challenging aspects of designing multithreaded applications is handling the relationship between the UI thread and other threads. It is critical that the background threads you use in your application do not directly interact with the application UI. If a background thread tries to modify a control in the UI of your application, the control can be left in an unknown state. This can cause major problems in your application that are difficult to diagnose. For example, a dynamically generated bitmap may be unable to render while another thread is feeding it new data. Or, a component bound to a dataset may display conflicting information while the dataset is being refreshed.

To avoid these problems, you should never allow threads other than the UI thread to make changes to UI controls, or to data objects bound to the UI. You should always try and maintain a strict separation between the UI code and the background processing code.

Separating the UI thread from the other threads is good practice, but you still need to pass information back and forth between the threads. Your multithreaded application will typically need to be capable of the following:

  • Obtaining the results from a background thread and updating the UI.
  • Reporting progress to the UI as a background thread performs its processing.
  • Controlling the background thread from the UI, for example letting the user cancel the background processing.

An effective way to separate the UI code from the code that handles the background thread is to structure your application in terms of tasks, and to represent each task using an object that encapsulates all of the task details.

A task is a unit of work that the user expects to be able to carry out within the application. In the context of multithreading, the Task object encapsulates all of the threading details so that they are cleanly separated from the UI.

By using the Task pattern, you can simplify your code when using multiple threads. The Task pattern clearly separates thread management code from UI code. The UI uses properties and methods provided by the Task object to perform actions such as starting and stopping tasks, and to query them for status. The Task object can also provide a number of events, allowing status information to be passed back to the UI. These events should all be fired on the UI thread so that the UI does not need to be aware of the background thread.

You can simplify thread interactions substantially by using a Task object that is responsible for controlling and managing the background thread but fires events that can be consumed by the UI and guaranteed to be on the UI thread. Task objects can be reused in various parts of the application, or even in other applications.

Figure 6.1 illustrates the overall structure of the code when you use the Task pattern.

Ff649143.multif01(en-us,PandP.10).gif

Figure 6.1   Code structure when using the Task pattern

Note   The Task pattern can be used to perform local background processing tasks on a separate thread or to interact with a remote service over the network asynchronously. In the latter case, the Task object is often called a service agent. A service agent can use the same pattern as the Task object and can support properties and events that make its interaction with the UI easier.

Because the Task object encapsulates the state of the task, you can use it to update the UI. To do so, you can have the Task object fire PropertyChanged events to the main UI thread whenever a change occurs. These events provide a standard, consistent way to communicate property value changes.

You can use tasks to inform the main UI thread of progress or other state changes. For example, when a task becomes available, you can set its enabled flag, which can be used to enable the corresponding menu item and toolbar buttons. Conversely, when a task becomes unavailable (for example, because it is in progress), you can set the enabled flag to false, which causes the event hander in the main UI thread to disable the correct menu items and toolbar buttons.

You can also use tasks to update data objects that are bound to the UI. You should ensure that any data objects that are data bound to UI controls are updated on the UI thread. For example, if you bind a DataSet object to the UI and retrieve updated information from a Web service, you can pass the new data to your UI code. The UI code then merges the new data into the bound DataSet on the UI thread.

You can use a Task object to implement background processing and threading control logic. Because the Task object encapsulates the necessary state and data, it can coordinate the work required to carry out the task on one or more threads and communicate changes and notifications to the application's UI as required. All required locking and synchronization can be implemented and encapsulated in the Task object, so that the UI thread does not have to deal with these issues.

Defining a Task Class

The following code example shows a class definition for a task that manages a long calculation.

Note   Although this example is simple, it can be easily extended to support complex background tasks that are integrated in the application's UI.
public class CalculationTask
{
    // Class Members…    public CalculationTask();
    public void StartCalculation( int count );
    public void StopCalculation();

    private void FireStatusChangedEvent( CalculationStatus status );
    private void FireProgressChangedEvent( int progress );
    private string Calculate( int count );
    private void EndCalculate( IAsyncResult ar );
}

The CalculationTask class defines a default constructor and two public methods for starting and stopping the calculation. It also defines helper methods that help the Task object to fire events to the UI. The Calculate method implements the calculation logic and is run on a background thread. The EndCalculate method implements the callback method, which is called after the background calculation thread has completed.

The class members are as follows:

private CalculationStatus _calcState;

private delegate string CalculationDelegate( int count );

public delegate void CalculationStatusEventHandler(
                object sender, CalculationEventArgs e );

public delegate void CalculationProgressEventHandler(
                object sender, CalculationEventArgs e );

public event CalculationStatusEventHandler CalculationStatusChanged;
public event CalculationProgressEventHandler CalculationProgressChanged;

The CalculationStatus member is an enumeration that defines the three states that the calculation can be in at any one time.

public enum CalculationStatus
{
    NotCalculating,
    Calculating,
    CancelPending
}

The Task class provides two events: one to inform the UI about calculation status events, and the other to inform the UI about calculation progress. The delegate signatures are defined as well as the events themselves.

The two events are fired in the helper methods. These methods check the type of the target; if the target's type is derived from the Control class, they fire the events by using the Invoke method on the control class. Therefore, for UI event sinks, the event is guaranteed to be called on the UI thread. The following example shows the code for firing the event.

private void FireStatusChangedEvent( CalculationStatus status )
{
    if( CalculationStatusChanged != null )
    {
        CalculationEventArgs args = new CalculationEventArgs( status );
        if ( CalculationStatusChanged.Target is
                System.Windows.Forms.Control )
        {
            Control targetForm = CalculationStatusChanged.Target
                    as System.Windows.Forms.Control;
            targetForm.Invoke( CalculationStatusChanged,
                    new object[] { this, args } );
        }
        else
        {
            CalculationStatusChanged( this, args );
        }
    }
}

This code first checks to see if an event sink has been registered, and if it has been registered, it checks the type of the target. If the target's type is derived from the Control class, the event is fired using the Invoke method to ensure that it is processed on the UI thread. If the target's type is not derived from the Control class, the event is fired normally. Events are fired in the same way to report calculation progress to the UI in the FireProgressChangedEvent method, as shown in the following example.

private void FireProgressChangedEvent( int progress )
    {
        if( CalculationProgressChanged != null )
        {
            CalculationEventArgs args =
                new CalculationEventArgs( progress );
            if ( CalculationProgressChanged.Target is
                    System.Windows.Forms.Control )
            {
                Control targetForm = CalculationProgressChanged.Target
                        as System.Windows.Forms.Control;
                targetForm.Invoke( CalculationProgressChanged,
                        new object[] { this, args } );
            }
            else
            {
                CalculationProgressChanged( this, args );
            }
        }
}

The CalculationEventArgs class defines the event arguments for both events and contains the calculation status and progress parameters so that they can be sent to the UI. The CalculationEventArgs class is defined as follows.

public class CalculationEventArgs : EventArgs
    {
        public string            Result;
        public int               Progress;
        public CalculationStatus Status;

        public CalculationEventArgs( int progress )
        {
            this.Progress = progress;
            this.Status   = CalculationStatus.Calculating;
        }

        public CalculationEventArgs( CalculationStatus status )
        {
            this.Status = status;
        }
}    

The StartCalculation method is responsible for starting the calculation on the background thread. The delegate CalculationDelegate allows the Calculation method to be invoked on a background thread using the Delegate Asynchronous Call pattern, as shown in the following example.

public void StartCalculation( int count )
{
    lock( this )
    {
        if( _calcState == CalculationStatus.NotCalculating )
        {
            // Create a delegate to the calculation method.
            CalculationDelegate calc =
                    new CalculationDelegate( Calculation );

            // Start the calculation.
            calc.BeginInvoke( count,
                    new AsyncCallback( EndCalculate ), calc );

            // Update the calculation status.
            _calcState = CalculationStatus.Calculating;

            // Fire a status changed event.
            FireStatusChangedEvent( _calcState );
        }
    }
}

The StopCalculation method is responsible for canceling the calculation, as shown in the following code example.

public void StopCalculation()
{
    lock( this )
    {
        if( _calcState == CalculationStatus.Calculating )
        {
            // Update the calculation status.
            _calcState = CalculationStatus.CancelPending;

            // Fire a status changed event.
            FireStatusChangedEvent( _calcState );
        }
    }
}

When StopCalculation is called, the calculation state is set to CancelPending to signal the background to stop the calculation. An event is fired to the UI to signal that the cancel request has been received.

Both of these methods use the lock keyword to ensure that the changes to the calculation state variable are atomic, so your application does not encounter a race condition. Both methods fire a status changed event to inform the UI that the calculation is starting or stopping.

The calculation method is defined as follows.

private string Calculation( int count )
{
    string result = "";
    for ( int i = 0 ; i < count ; i++ )
    {
        // Long calculation…        // Check for cancel.
        if ( _calcState == CalculationStatus.CancelPending ) break;

        // Update Progress
        FireProgressChangedEvent( count, i );
    }
    return result;
}
Note   For clarity, the details of the calculation have been omitted.

As each pass is made through the loop, the calculation state member is checked to see if the user has canceled the calculation. If so, the loop is exited, completing the calculation method. If the calculation continues, an event is fired, using the FireProgressChanged helper method, to report progress to the UI.

After the calculation is complete, the EndCalculate method is called to finish the asynchronous call by calling EndInvoke, as shown in the following example.

private void EndCalculate( IAsyncResult ar )
{
    CalculationDelegate del = (CalculationDelegate)ar.AsyncState;
    string result = del.EndInvoke( ar );

    lock( this )
    {
        _calcState = CalculationStatus.NotCalculating;
        FireStatusChangedEvent( _calcState );
    }
}

EndCalculate resets the calculation state to NotCalculating, ready for the next calculation to begin. It also fires a status changed event so that the UI can be notified that the calculation has been completed.

Using the Task Class

The Task class is responsible for managing background threads. To use the Task class, all you have to do is create a Task object, register the events that it fires, and implement the handling for these events. Because the events are fired on the UI thread, you don't need to worry about threading issues at all in your code.

The following example shows a Task object being created. In this example, the UI has two buttons, one for starting the calculation and one for stopping the calculation, and a progress bar that shows the current calculation progress.

// Create new task object to manage the calculation.
_calculationTask = new CalculationTask();

// Subscribe to the calculation status event.
_ calculationTask.CalculationStatusChanged += new
  CalculationTask.CalculationStatusEventHandler( OnCalculationStatusChanged );

// Subscribe to the calculation progress event.
_ calculationTask.CalculationProgressChanged += new
  CalculationTask.CalculationProgressEventHandler( 
OnCalculationProgressChanged );

The event handlers for the calculation status and calculation progress events update the UI appropriately, for example by updating a status bar control.

private void CalculationProgressChanged( object sender, 
CalculationEventArgs e )
{
    _progressBar.Value = e.Progress;
}

The CalculationStatusChanged event handler, which is shown in the following code, updates the value of a progress bar to reflect the current progress of the calculation. It is assumed that the minimum and maximum values of the progress bar have already been initialized.

private void CalculationStatusChanged( object sender, CalculationEventArgs e )
{
    switch ( e.Status )
    {
        case CalculationStatus.Calculating:
            button1.Enabled = false;
            button2.Enabled = true;
            break;

        case CalculationStatus.NotCalculating:
            button1.Enabled = true;
            button2.Enabled = false;
            break;

        case CalculationStatus.CancelPending:
            button1.Enabled = false;
            button2.Enabled = false;
            break;
    }
}

In this example, the CalculationStatusChanged event handler enables and disables the start and stop buttons depending on the calculation's status. This prevents the user from trying to start a calculation that is already in progress and provides feedback to the user about the status of the calculation.

The UI implements form event handlers for each button click to start and stop the calculation using the public methods on the Task object. For example, a start button event handler calls the StartCalculation method as follows.

private void startButton_Click( object sender, System.EventArgs e )
{
    calculationTask.StartCalculation( 1000 );
}

Similarly, a stop calculation button stops the calculation by calling the StopCalculation method as follows.

private void stopButton_Click( object sender, System.EventArgs e )
{
    calculationTask.StopCalculation();
}

Summary

Multithreading is an important part of creating responsive smart client applications. You should examine where multiple threads are appropriate for your application, looking to conduct all processing that does not involve the UI directly on separate threads. In most cases, you can use the ThreadPool class to create threads. However, in some cases you have to use the Thread class instead, and in others you need to use delegate objects or a Web service proxy to cause specific processing to occur on a non-UI thread.

In multithreaded applications, you must ensure that the UI thread is responsible for all UI-related tasks, and that you manage communication between the UI thread and other threads effectively. The Task pattern can help simplify this interaction significantly.

patterns & practices Developer Center

Show:
© 2014 Microsoft