Processes, Threads, and Asynchronous Programming
Asynchronous Programming Patterns
In this chapter, we're going to introduce four methods you can use to add multithreading to your programs. This chapter is a bit different from the previous chapters in that it goes beyond just the language features. Instead, we'll also include classes from the BCL and include some programming techniques. In spite of the fact that these things are a bit beyond just the language features, I want to do this because it's imperative that we as programmers increase our use of multiprocessing in our code—and I think a first book on C# is a good place to start.
When you start a program, the system creates a new process in memory. A process is the set of resources that comprise a running program. These include the virtual address space, file handles, and a host of other things required for the program to run.
Inside the process, the system creates a kernel object, called a thread, which represents the actual executing program. (Thread is short for “thread of execution.”) Once the process is set up, the system starts execution of the thread at the first statement in method Main
.
Some important things to know about threads are the following:
All the sample programs shown so far in this book have used only a single thread and have executed sequentially from the first statement in the program to the last. This is called synchronous programming. Asynchronous programming refers to programs that spawn multiple threads, which are, at least conceptually, executed at the same time. (They might not actually be executed at the same time.)
If the program is running on a multiprocessor system, the different threads might actually be executing at the same time on different processors. This can considerably improve performance, and as multicore processors become the norm, we need to write our programs to take advantage of this opportunity.
On a single-processor system, though, clearly only one instruction can be executed by the processor at a time. In this case, the operating system coordinates the threads so that the processor is shared among them. Each thread gets the processor for a short time, called a time slice, before being kicked off the processor and sent to the back of the line. This round-robin sharing of the processor lets all the threads work their ways through the code.
Using multiple threads in a program, called multithreading, or just threading, creates program overhead and additional program complexity. Here are some examples:
In spite of these considerations, the benefits of threading can outweigh its costs, as long as it's used wisely—and not overused. For example, you've already seen that on a multiprocessor system, if the different threads can be placed on different processors, it can result in a much more efficient execution.
To help alleviate some of the costs associated with creating and destroying threads, the CLR maintains a thread pool for each process. Initially, a process's thread pool is empty, but after a thread is created and used by a process and then the thread completes its execution, it isn't destroyed but instead added to the process's thread pool. Later, if the process needs another thread, the CLR recycles one from the pool, saving a significant amount of time.
Another common example where multithreading is crucial is in graphical user interface (GUI) programming, where users expect a quick response any time they click a button or use the keyboard. In this case, if the program needs to perform an operation that's going to take any appreciable time, it should perform that operation on another thread, leaving the main thread available to respond to the user's input. It would be totally unacceptable to have the program unresponsive during that time.
Although multithreading is conceptually easy, getting all the details right can be frustratingly difficult on nontrivial programs. The areas that need to be considered are the following:
The System.Threading
namespace contains classes and types that you can use to build complex multithreaded systems. These include the Thread
class itself and classes such as Mutex
, Semaphore
, and Monitor
, which are used to synchronize resource usage. The use, complexities, and nuances of this tricky subject are beyond the scope of this text, and you'd be better advised to settle down with an in-depth book on the subject.
.NET 4.0 has introduced a new library, called the Task Parallel Library, which greatly simplifies parallel programming. This is a huge advance and includes a large amount of material—far more than I can cover in this chapter. So unfortunately, I've had to settle by just whetting your appetite by introducing just two of its very simple constructs that you can learn and use quickly and easily. These are the Parallel.For
loop and the Parallel.ForEach
loop. These constructs are in the System.Threading.Tasks
namespace.
By this point in the book I'm sure you're quite familiar with C#'s standard for
and foreach
loops. These are common and tremendously powerful constructs. Many times when using these constructs, each iteration depends on a calculation or action in the previous iteration. But this isn't always the case. When the iterations are independent, it would be a huge advantage if you could put different iterations on different processors and process them in parallel. This is exactly what the Parallel.For
and Parallel.ForEach
constructs do.
These constructs are in the form of methods with input parameters. There are 12 overloads of the Parallel.For
method, but the simplest has the following signature:
index <
ToExclusive.void Parallel.For( int fromInclusive, int toExclusive, Action body);
The following code is an example using the Parallel.For
construct. It iterates from 0 to 15 and prints out the iteration index and the square of the index. Notice that it fits the requirement that each iteration is independent of any other iteration. Notice also that you must use the System.Threading.Tasks
namespace.
using System;
using System.Threading.Tasks; // Must use this namespace
namespace ExampleParallelFor
{
class Program
{
static void Main( )
{
Parallel.For( 0, 15, i =>
Console.WriteLine( "The square of {0} is {1}", i, i * i ));
}
}
}
One run of this code on a PC with a two-core processor produced the following output. Notice that you're not guaranteed any particular order of the iterations.
The square of 0 is 0
The square of 7 is 49
The square of 8 is 64
The square of 9 is 81
The square of 10 is 100
The square of 11 is 121
The square of 12 is 144
The square of 13 is 169
The square of 3 is 9
The square of 4 is 16
The square of 5 is 25
The square of 6 is 36
The square of 14 is 196
The square of 1 is 1
The square of 2 is 4
Another example is the following code. This program fills an integer array, in parallel, with the square of the iteration index.
class Program
{
static void Main()
{
const int maxValues = 50;
int[] squares = new int[maxValues];
Parallel.For( 0, maxValues, i =< squares[i] = i * i );
}
}
Unlike the previous example, even though the iterations might be executed in parallel and in any order, the end result is an array containing the first 50 squares.
The other parallel loop construct is the Parallel.ForEach
method. There are more than a dozen overloads for this method, but the simplest is the following:
TSource
is the type of object in the collection.TSource
objects.static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source,
Action<TSource> body)
An example of using the Parallel.ForEach
method is the following code. In this case, TSource
is string
, and the source is a string[]
.
using System;
using System.Threading.Tasks;
namespace ParallelForeach1
{
class Program
{
static void Main()
{
string[] squares = new string[]
{ "We", "hold", "these", "truths", "to", "be", "self-evident",
"that", "all", "men", "are", "created", "equal"};
Parallel.ForEach( squares,
i => Console.WriteLine( string.Format("{0} has {1} letters", i, i.Length) ));
}
}
}
One run of this code on a PC with a two-core processor produced the following output, but the order might change each time:
"We" has 2 letters
"equal" has 5 letters
"truths" has 6 letters
"to" has 2 letters
"be" has 2 letters
"that" has 4 letters
"hold" has 4 letters
"these" has 5 letters
"all" has 3 letters
"men" has 3 letters
"are" has 3 letters
"created" has 7 letters
"self-evident" has 12 letters
Although much of asynchronous programming is complex, the BackgroundWorker
class makes it simple to perform a task in the background on a separate thread. This class was designed primarily for GUI programming (Windows Forms and WPF) to allow them to offload time-consuming tasks from the main thread to a background thread. Figure 22-1 illustrates the key members of the class. The following is an overview of these members:
DoWork
event is raised when the background thread starts.ProgressChanged
event is raised when the background task reports progress.RunWorkerCompleted
event is raised when the background worker exits.RunWorkerAsync
method retrieves a background thread that executes the DoWork
event handler.CancelAsync
method sets the CancellationPending
property to true
, potentially, although not necessarily, canceling the thread. It is the responsibility of the DoWork
event handler to inspect this property to determine whether it should stop its processing.ReportProgress
method can be called by the DoWork
event handler (from the background thread) when it wants to report its progress to the main thread.Figure 22-1. The key members of the BackgroundWorker class
To use a BackgroundWorker
class object, you need to write the following event handlers. The first is required since it contains the code you want to be executed by the background thread, but the other two are optional, depending on the needs of your program.
DoWork
event contains the code you want executed in the background on a separate thread.
DoTheWork
and is in a gradient-shaded box to illustrate that it's executed in the separate thread.DoWork
event is raised when the main thread calls the RunWorkerAsync
method.ProgressChanged
event should contain the code to be executed on the main thread when the background task reports its progress.
ProgressChanged
event is raised when the background process calls the ReportProgress
method.ReportProgress
method is how the background thread communicates with the main thread.RunWorkerCompleted
event should contain the code to be executed on the main thread after the background thread completes the execution of the DoWork
event handler.Figure 22-2 shows the structure of your program, with the event handlers attached to the events of the BackgroundWorker
object.
Figure 22-2. Your code supplies event handlers for the events that control the flow through execution of the tasks.
The delegates for these event handlers are the following. Each takes an object
reference as the first parameter and a specialized subclass of the EventArgs
class as the second parameter.
void DoWorkEventHandler ( object sender, DoWorkEventArgs e )
void ProgressChangedEventHandler ( object sender, ProgressChangedEventArgs e )
void RunWorkerCompletedEventHandler ( object sender, RunWorkerCompletedEventArgs e)
Figure 22-3 illustrates the structure of the EventArg
classes used by these event handlers.
Figure 22-3. The EventArg classes used by the BackgroundWorker event handlers
When you have the event handlers written and attached to their events, you can use the class by doing the following:
BackgroundWorker
class and configuring it.
WorkerReportsProgress
property to true
.WorkerSupportsCancellation
property to true
.RunWorkerAsync
method. This retrieves a background thread that raises the DoWork
event and executes the event's handler in the background.Now you have both the main thread and the background thread running. While the background thread is running, you can continue processing on the main thread.
WorkerSupportsCancellation
property, then you can call the object's CancelAsync
method. This does not cancel the background thread! Instead, it sets the object's CancellationPending
property to true
that needs to be checked by the DoWork
event handler code running on the background thread.WorkerReportsProgress
property is true
and the background thread has progress to report to the main thread, then it must call the BackgroundWorker
object's ReportProgress
method. When the background thread calls the ReportProgress
method, this raises the ProgressChanged
event in the main thread, which runs the corresponding event handler.WorkerSupportsCancellation
property is enabled, then the DoWork
event handler code should regularly check the CancellationPending
property to determine whether it has been canceled. If so, it should exit.Result
field in the DoWorkEventArgs
parameter shown previously, in Figure 22-3.RunWorkerCompleted
event is raised, and its handler is executed on the main thread. The RunWorkerCompletedEventArgs
parameter can contain information from the now completed background thread, such as the return value and whether the thread was canceled.Although the BackgroundWorker
class was designed for GUI programming, I'll start by showing its use with a console program since that's what we've used throughout the book. In the next section, I'll show an example with a GUI program.
This program creates a background thread that sums a sequence of numbers. Several times during the process it checks to see whether it has been canceled. If it finds it's been canceled, it cleans up and exits. Otherwise, if it goes to completion, it stores the total in the Result
field and exits.
Meanwhile, the main thread sums its own sequence of numbers and reports its total, along with the result from the background thread.
using System;
using System.ComponentModel; // Must have this namespace
using System.Threading; // Must have this namespace
namespace ConsoleBackgroundWorker
{
class DoBackgroundwork
{
BackgroundWorker bgWorker = new BackgroundWorker();
public long BackgroundTotal { get; private set; }
public bool CompletedNormally { get; private set; }
// Constructor
public DoBackgroundwork()
{
// Set BackgroundWorker properties
bgWorker.WorkerReportsProgress = true;
bgWorker.WorkerSupportsCancellation = true;
// Connect handlers to BackgroundWorker object.
bgWorker.DoWork += DoWork_Handler;
bgWorker.ProgressChanged += ProgressChanged_Handler;
bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler;
}
public void StartWorker()
{
if ( !bgWorker.IsBusy )
bgWorker.RunWorkerAsync();
}
// This just calculates the sum of the integers from 0 to the input value.
public static long CalculateTheSequence( long value )
{
long total = 0;
for ( int i=0; i < value; i++ )
total += i;
return total;
}
public void DoWork_Handler( object sender, DoWorkEventArgs args )
{
BackgroundWorker worker = sender as BackgroundWorker;
// Do the background calculation
long total = 0;
for ( int i = 1; i <= 5; i++ )
{
// Each time through the loop, check to see if we've been cancelled
if ( worker.CancellationPending )
{
args.Cancel = true;
worker.ReportProgress( -1 );
break;
}
else
{
// If we haven't been cancelled, then continue the calculation.
total += CalculateTheSequence( i * 10000000 );
worker.ReportProgress( i * 20 );
// Slow the program down to a more comfortable output rate
// just for this demo.
Thread.Sleep( 300 );
}
}
args.Result = total; // Store the result and exit.
}
// Handle input from background thread.
private void ProgressChanged_Handler
( object sender, ProgressChangedEventArgs args )
{
string output
= args.ProgressPercentage == -1
? " Cancelled"
: string.Format(" {0}%", args.ProgressPercentage );
Console.WriteLine( output );
}
// On completion of background thread, summarize and store the result.
private void RunWorkerCompleted_Handler
( object sender, RunWorkerCompletedEventArgs args )
{
CompletedNormally = !args.Cancelled;
BackgroundTotal = args.Cancelled
? 0
: (long) args.Result; // Cast from object
}
public void Cancel()
{
if( bgWorker.IsBusy )
bgWorker.CancelAsync();
}
}
class Program
{
static void Main()
{
GiveInstructionsToTheUser();
OutputTheSummaryHeaders();
// Create and Start the background worker
DoBackgroundwork bgw = new DoBackgroundwork();
bgw.StartWorker();
// Start the computation on the main thread. Each time through the loop,
// check to see whether the user has cancelled the background thread.
// After the calculation, add a short sleep, just to slow the program
// down enough so the main thread doesn't run faster than the background.
long mainTotal = 0;
for ( int i = 0; i < 5; i++ )
{
if ( Program.CheckForCancelInput() )
bgw.Cancel();
mainTotal += DoBackgroundwork.CalculateTheSequence( 100000000 );
Thread.Sleep( 200 );
Console.WriteLine( " {0}%", (i+1) * 20 );
}
SummarizeResults( bgw, mainTotal );
Console.ReadLine();
}
private static void GiveInstructionsToTheUser()
{
Console.WriteLine( "Press <Enter> to start background worker." );
Console.WriteLine( "Press <Enter> again to cancel background worker." );
Console.ReadLine();
}
private static void OutputTheSummaryHeaders()
{
Console.WriteLine( " Main Background" );
Console.WriteLine( "---------------------" );
}
private static void SummarizeResults( DoBackgroundwork bgw, long mainTotal )
{
if ( bgw.CompletedNormally )
{
Console.WriteLine( "
Background completed Normally" );
Console.WriteLine( "Background total = {0}", bgw.BackgroundTotal );
}
else
{
Console.WriteLine( "
Background Cancelled" );
}
Console.WriteLine( "Main total = {0}", mainTotal );
}
private static bool CheckForCancelInput()
{
bool doCancel = Console.KeyAvailable;
if ( doCancel )
Console.ReadKey();
return doCancel;
}
}
}
This code produces the following results when allowed to run to completion.
Press <Enter> to start background worker.
Press <Enter> again to cancel background worker.
Main Background
---------------------
20%
40%
20%
60%
40%
80%
60%
100%
80%
100%
Background completed Normally
Background total = 2749999925000000
Main total = 24999999750000000
Since the BackgroundWorker
class is primarily used with GUI programming, the following program shows its use in a simple WPF program rather than the console programs we've used throughout the text. WPF is Microsoft's replacement for the Windows Forms GUI programming framework. For further information about WPF programming, please see my book Illustrated WPF, also published by Apress.
This program produces the window shown on the left in Figure 22-4. When you click the Process button, it starts the background thread, which reports to the main thread every half second and increments the progress bar at the top by 10 percent. At completion, it shows the dialog box on the right of Figure 22-4.
Figure 22-4. The example WPF program using the BackgroundWorker class
To create this WPF program in Visual Studio 2010, do the following:
There are only two files you'll modify—MainWindow.xaml
and MainWindow.xaml.cs
. Modify your MainWindow.xaml
file to match the following listing:
<Window x:Class="SimpleWorker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="150 " Width="250">
<StackPanel>
<ProgressBar Name="progressBar" Height="20" Width="200" Margin="10"/>
<Button Name="btnProcess" Width="100" Click="btnProcess_Click"
Margin="5">Process</Button>
<Button Name="btnCancel" Width="100" Click="btnCancel_Click"
Margin="5">Cancel</Button>
</StackPanel>
</Window>
Modify your MainWindow.xaml.cs
file to match the following listing:
using System.Windows;
using System.ComponentModel;
using System.Threading;
namespace SimpleWorker
{
public partial class MainWindow : Window
{
BackgroundWorker bgWorker = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
// Set BackgroundWorker properties
bgWorker.WorkerReportsProgress = true;
bgWorker.WorkerSupportsCancellation = true;
// Connect handlers to BackgroundWorker object.
bgWorker.DoWork += DoWork_Handler;
bgWorker.ProgressChanged += ProgressChanged_Handler;
bgWorker.RunWorkerCompleted += RunWorkerCompleted_Handler;
}
private void btnProcess_Click( object sender, RoutedEventArgs e )
{
if ( !bgWorker.IsBusy )
bgWorker.RunWorkerAsync();
}
private void ProgressChanged_Handler( object sender,
ProgressChangedEventArgs args )
{
progressBar.Value = args.ProgressPercentage;
}
private void DoWork_Handler( object sender, DoWorkEventArgs args )
{
BackgroundWorker worker = sender as BackgroundWorker;
for ( int i = 1; i <= 10; i++ )
{
if ( worker.CancellationPending )
{
args.Cancel = true;
break;
}
else
{
worker.ReportProgress( i * 10 );
Thread.Sleep( 500 );
}
}
}
private void RunWorkerCompleted_Handler( object sender,
RunWorkerCompletedEventArgs args )
{
progressBar.Value = 0;
if ( args.Cancelled )
MessageBox.Show( "Process was cancelled.", "Process Cancelled" );
else
MessageBox.Show( "Process completed normally.", "Process Completed" );
}
private void btnCancel_Click( object sender, RoutedEventArgs e )
{
bgWorker.CancelAsync();
}
}
}
In Chapter 15, we covered the topic of delegates, and you saw that when a delegate object is invoked, it invokes the methods contained in its invocation list. This is done synchronously, just as if the methods had been called by the program.
If a delegate object has only a single method (which I'll call the referenced method) in its invocation list, it can execute that method asynchronously. The delegate class has two methods, called BeginInvoke
and EndInvoke
, that are used to do this. You use these methods in the following way:
BeginInvoke
method, it starts its referenced method executing on a separate thread from the thread pool and then returns immediately to the initial thread. The initial thread then continues on while the referenced method executes in parallel.IsCompleted
property of the IAsyncResult
returned by BeginInvoke
or calls the delegate's EndInvoke
method to wait for the delegate to finish.Figure 22-5 shows the three standard patterns for using this process. In all three patterns, the initial thread initiates an asynchronous method call and then does some additional processing. The patterns differ, however, in the ways in which the initial thread receives the information that the spawned thread has completed.
EndInvoke
.Figure 22-5. The standard patterns for asynchronous method calls
Before we look at examples of these asynchronous programming patterns, let's take a closer look at the BeginInvoke
and EndInvoke
methods. Some of the important things to know about BeginInvoke
are the following:
BeginInvoke
, the actual parameters in the parameter list consist of the following:
callback
parameter and the state
parameterBeginInvoke
retrieves a thread from the thread pool and starts the referenced method running on the new thread.BeginInvoke
returns to the calling thread a reference to an object implementing the IAsyncResult
interface. This interface reference contains information about the current state of the asynchronous method. The initial thread then continues execution.The following code shows an example of calling a delegate's BeginInvoke
method. The first line declares a delegate type called MyDel
. The next line declares a method called Sum
, which matches the delegate.
del
, of the delegate type MyDel
, and initializes its invocation list with the Sum
method.BeginInvoke
method of the delegate object and supplies it with the two delegate parameters 3
and 5
and the two BeginInvoke
parameters callback
and state
, which are set to null
in this example. When executed, the BeginInvoke
method performs two actions:
Sum
running on the new thread, supplying it with 3
and 5
as its actual parameters.IAsyncResult
, which it returns to the calling thread. The calling thread, in this example, stores it in a variable called iar
.You use the EndInvoke
method to retrieve the values returned by the asynchronous method call and to release resources used by the thread. EndInvoke
has the following characteristics:
IAsyncResult
returned by the BeginInvoke
method and finds the thread it refers to.EndInvoke
does the following:
EndInvoke
is called, the calling thread stops and waits for it to finish before cleaning up and returning the value. Because EndInvoke
cleans up after the spawned thread, you must make sure that an EndInvoke
is called for each BeginInvoke
.EndInvoke
is called.The following line of code shows an example of calling EndInvoke
to retrieve the value from an asynchronous method. You must always include the reference to the IAsyncResult
object as a parameter.
EndInvoke
supplies all the output from the asynchronous method call, including ref
and out
parameters. If a delegate's referenced method has ref
or out
parameters, they must be included in EndInvoke
's parameter list before the reference to the IAsyncResult
object, as shown here:
Now that you understand the BeginInvoke
and EndInvoke
delegate methods, we can look at the asynchronous programming patterns. The first one we'll look at is the wait-until-done pattern. In this pattern, the initial thread initiates an asynchronous method call, does some additional processing, and then stops and waits until the spawned thread finishes. It's summarized as follows:
IAsyncResult iar = del.BeginInvoke( 3, 5, null, null );
// Do additional work in the calling thread, while the method
// is being executed asynchronously in the spawned thread.
...
long result = del.EndInvoke( iar );
The following code shows a full example of this pattern. This code uses the Sleep
method of the Thread
class to suspend itself for 100 milliseconds (1/10 of a second). The Thread
class is in the System.Threading
namespace.
using System;
using System.Threading; // For Thread.Sleep()
delegate long MyDel( int first, int second ); // Declare delegate type
class Program {
static long Sum(int x, int y) // Declare method for async
{
Console.WriteLine(" Inside Sum");
Thread.Sleep(100);
return x + y;
}
static void Main( ) {
MyDel del = new MyDel(Sum);
Console.WriteLine( "Before BeginInvoke" );
IAsyncResult iar = del.BeginInvoke(3, 5, null, null); // Start async
Console.WriteLine( "After BeginInvoke" );
Console.WriteLine( "Doing stuff" );
long result = del.EndInvoke( iar ); // Wait for end and get result
Console.WriteLine( "After EndInvoke: {0}", result );
}
}
This code produces the following output:
Before BeginInvoke
After BeginInvoke
Doing stuff
Inside Sum
After EndInvoke: 8
Now that you've seen BeginInvoke
and EndInvoke
in action in their simplest forms, it's time to take a closer look at IAsyncResult
, which is an integral part of using these methods.
BeginInvoke
returns a reference to an IAsyncResult
interface that is inside a class object of type AsyncResult
. The AsyncResult
class represents the state of the asynchronous method. Figure 22-6 shows a representation of some of the important parts of the class. The important things to know about the class are the following:
BeginInvoke
method, the system creates an object of the class AsyncResult
. It doesn't, however, return a reference to the class object. Instead, it returns a reference to the interface contained in the object—IAsyncResult
.AsyncResult
object contains a property called AsyncDelegate
, which returns a reference to the delegate that was invoked to start the asynchronous method. This property, however, is part of the class object but not part of the interface.IsCompleted
property returns a Boolean value indicating whether the asynchronous method has completed.AsyncState
property returns a reference to the object that was listed as the state
parameter in the BeginInvoke
method invocation. It returns a reference of type object
. I'll explain this in the section on the callback pattern.Figure 22-6. An AsyncResult class object
In the polling pattern, the initial thread initiates an asynchronous method call, does some additional processing, and then uses the IsCompleted
method of the IAsyncResult
object to check periodically whether the spawned thread has completed. If the asynchronous method has completed, the initial thread calls EndInvoke
and continues. Otherwise, it does some additional processing and checks again later. The “processing” in this example just consists of counting from 0 to 10,000,000.
This code produces the following output:
After BeginInvoke
Not Done
Inside Sum
Not Done
Not Done
Done
Result: 8
In the previous two patterns, wait-until-done and polling, the initial thread continues with its flow of control only after it knows that the spawned thread has completed. It then retrieves the results and continues.
The callback pattern is different in that once the initial thread spawns the asynchronous method, it goes on its way without synchronizing with it again. When the asynchronous method call completes, the system invokes a user-supplied method to handle its results and to call the delegate's EndInvoke
method. This user-defined method is called a callback method, or just a callback.
The two extra parameters at the end of the BeginInvoke
parameter list are used with the callback method as follows:
callback
parameter, is the name of the callback method.state
parameter, can be either null
or a reference to an object you want passed into the callback method. You'll be able to access this object through the method's IAsyncResult
parameter using its AsyncState
property. The type of this parameter is object
.The signature and return type of the callback method must be of the form described by the AsyncCallback
delegate type. This form requires that the method take a single parameter of type IAsyncResult
and have a void
return type, as shown here:
void AsyncCallback( IAsyncResult iar )
There are several ways you can supply the callback method to the BeginInvoke
method. Since the callback
parameter in BeginInvoke
is a delegate of type AsyncCallback
, you can supply it as a delegate, as shown in the first code statement that follows. Or you can just supply the name of the callback method and let the compiler create the delegate for you. Both forms are semantically equivalent.
The second BeginInvoke
parameter is used to send an object to the callback method. It can be an object of any type, but the parameter is of type object
, so inside the callback method you'll have to cast it to the correct type.
Inside the callback method, your code should call the delegate's EndInvoke
method and take care of handling the output results of the asynchronous method execution. To call the delegate's EndInvoke
method, though, you need a reference to the delegate object, which is in the initial thread—not here in the spawned thread.
If you're not using BeginInvoke
's state
parameter for anything else, you can use it to send the delegate reference to the callback method, as shown here:
Otherwise, you can extract the delegate's reference from the IAsyncResult
object sent into the method as the parameter. This is shown in the following code and illustrated in Figure 22-7.
IAsyncResult
interface of the asynchronous method that has just completed. Remember that the IAsyncResult
interface object is inside the AsyncResult
class object.IAsyncResult
interface doesn't have a reference to the delegate object, the AsyncResult
class object enclosing it does have a reference to the delegate object. So, the first line inside the example method body gets a reference to the class object by casting the interface reference to the class type. Variable ar
now has a reference to the class object.AsyncDelegate
property of the class object and cast it to the appropriate delegate type. This gives you the delegate reference, which you can then use to call EndInvoke
.using System.Runtime.Remoting.Messaging; // Contains AsyncResult class
void CallWhenDone( IAsyncResult iar )
{
AsyncResult ar = (AsyncResult) iar; // Get class object reference
MyDel del = (MyDel) ar.AsyncDelegate; // Get reference to delegate
long Sum = del.EndInvoke( iar ); // Call EndInvoke
...
}
Figure 22-7. Extracting the delegate's reference inside the callback method
The following code puts it all together and is an example of using the callback pattern.
using System;
using System.Runtime.Remoting.Messaging; // To access the AsyncResult type
using System.Threading;
delegate long MyDel(int first, int second);
class Program
{
static long Sum(int x, int y)
{
Console.WriteLine(" Inside Sum");
Thread.Sleep(100);
return x + y;
}
static void CallWhenDone(IAsyncResult iar)
{
Console.WriteLine(" Inside CallWhenDone.");
AsyncResult ar = (AsyncResult) iar;
MyDel del = (MyDel)ar.AsyncDelegate;
long result = del.EndInvoke(iar);
Console.WriteLine
(" The result is: {0}.", result);
}
static void Main()
{
MyDel del = new MyDel(Sum);
Console.WriteLine("Before BeginInvoke");
IAsyncResult iar =
del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);
Console.WriteLine("Doing more work in Main.");
Thread.Sleep(500);
Console.WriteLine("Done with Main. Exiting.");
}
}
This code produces the following output:
Before BeginInvoke
Doing more work in Main.
Inside Sum
Inside CallWhenDone.
The result is: 8.
Done with Main. Exiting.
Timers provide another way to run an asynchronous method on a regular, recurring basis. Although there are several Timer
classes available in the .NET BCL, I'll describe the one in the System.Threading
namespace.
The important things to know about this timer class are the following:
TimerCallback
delegate, which has the following form. It takes a single parameter of type object
and has a void
return type.void TimerCallback( object state )
state
object as its parameter, and starts it running.dueTime
is the amount of time before the first call of the callback method. If dueTime
is set to the special value Timeout.Infinite
, the timer will not start. If it's set to 0
, the callback is called immediately.period
is the amount of time between each successive call of the callback method. If it's value is set to Timeout.Infinite
, the callback won't be called after the first time.state
is either null
or a reference to an object to be passed to the callback method each time it's executed.The constructor for the Timer
class takes as parameters the name of the callback method, the dueTime
, the period
, and the state
. There are several constructors for Timer
; the one that's probably the most commonly used has the following form:
Timer( TimerCallback callback, object state, uint dueTime, uint period)
The following code statement shows an example of the creation of a Timer
object:
Once a Timer
object is created, you can change its dueTime
or period
using the Change
method.
The following code shows an example of using a timer. The Main
method creates the timer so that it will call the callback for the first time after two seconds and once every second after that. The callback method simply prints out a message, including the number of times it's been called.
This code produces the following output before being terminated after about five seconds:
Timer started.
Processing timer event 1
Processing timer event 2
Processing timer event 3
Processing timer event 4
There are several other timer classes supplied by the .NET BCL, each having its own uses. The other timer classes are the following:
System.Windows.Forms.Timer
: This class is used in Windows Forms applications to periodically place WM_TIMER
messages into the program's message queue. When the program gets the message from the queue, it processes the handler synchronously on the main user interface thread. This is extremely important in Windows Forms applications.System.Timers.Timer
: This class is more extensive and contains a number of members for manipulating the timer through properties and methods. It also has a member event called Elapsed
, which is raised when each period expires. This timer can run on either a user interface thread or a worker thread.3.131.38.14