Asynchronous Operations in C#

1. Asynchronous Calls

When using threads, the programmer is responsible for taking care of all the details of execution and determining how to transfer data from the caller to the thread and then back from the thread. This normally involves creating a class to encapsulate the data to be exchanged, which is a fair bit of extra work.

Writing this class isn’t difficult, but it’s a bit of a pain to do if all that’s needed is a single asynchronous call. Luckily, the runtime and compiler provide a way to get asynchronous execution without a separate class.

The runtime will handle the details of managing the thread on which the function will be called (using a thread pool) and provide an easy mechanism for exchanging data. Nicer still, the runtime will allow any function to be called with this mechanism; it doesn’t have to be designed to be asynchronous to call it asynchronously. This can be a nice way to start an oper­ation and then continue with the main code.

It all happens through a little magic in delegates.

To set up an asynchronous call, the first step is to define a delegate that matches the function to be called. For example, if the function is as follows:

Console.WriteLine(string s);

the delegate would be like this:

delegate void FuncToCall(string s);

If you place this delegate in a class and compile it, you can view it using the ILDASM utility. An Invoke() member takes a string, which invokes a delegate, and then there are two strange- looking functions:

public IAsyncResult BeginInvoke(string s, System.AsyncCallback callback, object o);

public void EndInvoke(IAsyncResult);

These functions are generated by the compiler to make doing asynchronous calls easier and are defined based upon the parameters and return type of the delegate, as detailed in Table 31-1.

In addition to the parameters defined for the delegate, BeginInvoke() also takes an optional callback to call when the function call has completed and an object that can be used by the caller to pass some state information. BeginInvoke() returns an IAsyncResult that’s passed to EndInvoke().

1.1. A Simple Example

The following example shows a simple async call:

using System;

public class AsyncCaller

{

// Declare a delegate that will match Console.WriteLine(“string”); delegate void FuncToCall(string s);

public void CallWriteLine(string s)

{

// delegate points to function to call

// start the async call

// wait for completion

FuncToCall func = new FuncToCall(Console.WriteLine);

IAsyncResult iar = func.BeginInvoke(s, null, null);

func.EndInvoke(iar);

}

}

class Test {

public static void Main()

{

AsyncCaller ac = new AsyncCaller();

ac.CallWriteLine(“Hello”);

}

}

The CallWriteLine() function takes a string parameter, creates a delegate to Console.WriteLine(), and then calls BeginInvoke() and EndInvoke() to call the function asynchronously and wait for it to complete.

That’s not terribly exciting. Let’s modify the example to use a callback function:

using System;

public class AsyncCaller {

// Declare a delegate that will match Console.WriteLine(“string”); delegate void FuncToCall(string s);

public void WriteLineCallback(IAsyncResult iar)

{

Console.WriteLine(“In WriteLineCallback”);

FuncToCall func = (FuncToCall) iar.AsyncState;

func.EndInvoke(iar);

}

public void CallWriteLineWithCallback(string s)

{

FuncToCall func = new FuncToCall(Console.WriteLine);

func.BeginInvoke(s,

new AsyncCallback(WriteLineCallback), func);

}

}

class Test {

public static void Main()

{

AsyncCaller ac = new AsyncCaller();

ac.CallWriteLineWithCallback(“Hello There”);

System.Threading.Thread.Sleep(1000);

}

}

The CallWriteLineWithCallback() junction calls BeginInvoke(), passing a callback function and the delegate. The callback routine takes the callback function passed in the state object and calls EndInvoke().

Because the call to CallWriteLineWithCallback() returns immediately, the Main() function sleeps for a second so the asynchronous call can complete before the program exits.

1.2. Return Values

This example calls Math.Sin() asynchronously:

using System;

using System.Threading;

public class AsyncCaller {

public delegate double MathFunctionToCall(double arg);

public void MathCallback(IAsyncResult iar)

{

MathFunctionToCall mc = (MathFunctionToCall) iar.AsyncState;

double result = mc.EndInvoke(iar);

Console.WriteLine(“Function value = {0}”, result);

}

public void CallMathCallback(MathFunctionToCall mathFunc,

double start,

double end,

double increment)

{

AsyncCallback cb = new AsyncCallback(MathCallback);

while (start < end)

{

Console.WriteLine(“BeginInvoke: {0}”, start);

mathFunc.BeginInvoke(start, cb, mathFunc);

start += increment;

}

}

}

class Test {

public static void Main()

{

AsyncCaller ac = new AsyncCaller();

ac.CallMathCallback(

new AsyncCaller.MathFunctionToCall(Math.Sin), 0.0, 1.0, 0.2);

Thread.Sleep(2000);

}

}

This generates the following output:

BeginInvoke: 0

BeginInvoke: 0.2

BeginInvoke: 0.4

BeginInvoke: 0.6

BeginInvoke: 0.8

Function value = 0

Function value = 0.198669330795061

Function value = 0.389418342308651

Function value = 0.564642473395035

Function value = 0.717356090899523

This time, the call to EndInvoke() in the callback returns the result of the function, which is then written out. Note that BeginInvoke() gets called before any of the calls to Math.Sin() occur.

Because the example doesn’t contain any synchronization, the call to Thread .Sleep is required to make sure the callbacks execute before main finishes. It’s worth noting that the call to Thread .Sleep is only an example, and you should use the proper synchronization in production code.

1.3. Waiting for Completion

It’s possible to wait for several asynchronous calls to finish, using WaitHandle as in the threads section. The IAsyncResult returned from BeginInvoke() has an AsyncWaitHandle member that can be used to know when the asynchronous call completes. Here’s a modification to the previous example:

using System;

using System.Threading;

public class AsyncCaller {

public delegate double MathFunctionToCall(double arg);

public void MathCallback(IAsyncResult iar)

{

MathFunctionToCall mc = (MathFunctionToCall) iar.AsyncState;

double result = mc.EndInvoke(iar);

Console.WriteLine(“Function value = {0}”, result);

}

WaitHandle DoInvoke(MathFunctionToCall mathFunc, double value)

{

AsyncCallback cb = new AsyncCallback(MathCallback);

IAsyncResult asyncResult =

mathFunc.BeginInvoke(value, cb, mathFunc);

return(asyncResult.AsyncWaitHandle);

}

public void CallMathCallback(MathFunctionToCall mathFunc)

{

WaitHandle[] waitArray = new WaitHandle[4];

Console.WriteLine(“Begin Invoke”);

waitArray[0] = DoInvoke(mathFunc, 0.1);

waitArray[1] = DoInvoke(mathFunc, 0.5);

waitArray[2] = DoInvoke(mathFunc, 1.0);

waitArray[3] = DoInvoke(mathFunc, 3.14159);

Console.WriteLine(“Begin Invoke Done”);

Console.WriteLine(“Waiting for completion”);

WaitHandle.WaitAll(waitArray, 10000, false);

Console.WriteLine(“Completion achieved”);

}

}

public class Test

{

public static double DoCalculation(double value)

{

Console.WriteLine(“DoCalculation: {0}”, value);

Thread.Sleep(250);

return(Math.Cos(value));

}

public static void Main()

{

AsyncCaller ac = new AsyncCaller();

ac.CallMathCallback(new AsyncCaller.MathFunctionToCall(DoCalculation));

//Thread.Sleep(500);                // no longer needed

}

}

The DoInvoke() function returns the WaitHandle for a specific call, and CallMathCallback() waits for all the calls to complete and then returns. Because of the wait, the sleep call in Main is no longer needed.

This example generates the following output:

Begin Invoke

Begin Invoke Done

Waiting for completion

DoCalculation: 0.1

Function value = 0.995004165278026

DoCalculation: 0.5

Function value = 0.877582561890373

DoCalculation: 1

Function value = 0.54030230586814

DoCalculation: 3.14159 Completion achieved

The return value for the last calculation is missing.

This illustrates a problem with using the WaitHandle that’s provided in the IAsyncResult. The WaitHandle is set when EndInvoke() is called but before the callback routine completes. In this example, it’s obvious that something’s wrong, but in a real program, this could result in a really nasty race condition, where some results are dropped. This means that using the provided WaitHandle is safe only if there isn’t any processing done after EndInvoke().

The way to deal with this problem is to ignore the provided WaitHandle and add a WaitHandle that’s called at the end of the callback function:

using System;

using System.Threading;

public class AsyncCallTracker {

Delegate function;

AutoResetEvent doneEvent;

public AutoResetEvent DoneEvent {

get

{

return(doneEvent);

}

}

public Delegate Function {

get

{

return(function);

}

}

public AsyncCallTracker(Delegate function)

{

this.function = function;

doneEvent = new AutoResetEvent(false);

}

}

public class AsyncCaller {

public delegate double MathFunctionToCall(double arg);

public void MathCallback(IAsyncResult iar)

{

AsyncCallTracker callTracker = (AsyncCallTracker) iar.AsyncState;

MathFunctionToCall func = (MathFunctionToCall) callTracker.Function;

double result = func.EndInvoke(iar);

Console.WriteLine(“Function value = {0}”, result);

callTracker.DoneEvent.Set();

}

WaitHandle DoInvoke(MathFunctionToCall mathFunc, double value)

{

AsyncCallTracker callTracker = new AsyncCallTracker(mathFunc);

AsyncCallback cb = new AsyncCallback(MathCallback);

IAsyncResult asyncResult = mathFunc.BeginInvoke(value, cb, callTracker);

return(callTracker.DoneEvent);

}

public void CallMathCallback(MathFunctionToCall mathFunc)

{

WaitHandle[] waitArray = new WaitHandle[4];

Console.WriteLine(“Begin Invoke”);

waitArray[0] = DoInvoke(mathFunc, 0.1);

waitArray[1] = DoInvoke(mathFunc, 0.5);

waitArray[2] = DoInvoke(mathFunc, 1.0);

waitArray[3] = DoInvoke(mathFunc, 3.14159);

Console.WriteLine(“Begin Invoke Done”);

Console.WriteLine(“Waiting for completion”);

WaitHandle.WaitAll(waitArray, 10000, false);

Console.WriteLine(“Completion achieved”);

}

}

public class Test {

public static double DoCalculation(double value)

{

Console.WriteLine(“DoCalculation: {0}”, value);

Thread.Sleep(250);

return(Math.Cos(value));

}

public static void Main()

{

AsyncCaller ac = new AsyncCaller();

ac.CallMathCallback(new AsyncCaller.MathFunctionToCall(DoCalculation));

}

}

It’s now necessary to pass both the delegate and an associated AutoResetEvent to the call­back function, so these are encapsulated in the AsyncCallTracker class. The AutoResetEvent is returned from DoInvoke(), and this event isn’t set until the last line of the callback, so there are no longer any race conditions.

2. Classes That Support Asynchronous Calls Directly

Some framework classes provide explicit support for asynchronous calls, which allows them to have full control over how asynchronous calls are processed. The HttpWebRequest class, for example, provides BeginGetResponse() and EndGetResponse() functions, so creating a delegate isn’t required. Windows Forms also has its own built-in threading support to allow a somewhat simplified model to work around the fact that the underlying Windows API isn’t thread-safe. Control.BeginInvoke (inherited by many classes in Windows.Forms) and Control.IsInvokeRequired are the workhorses of the Windows Forms threading functionality.

All the framework classes that provide such support adhere to the same pattern as the do- it-yourself approach and are used in the same manner.

2.1. Design Guidelines

Both threading and asynchronous calls provide a way to have more than one path of execution happen at once. In most situations, you can use either method.

Asynchronous calls are best for situations where you’re doing one or two asynchronous calls and you don’t want the hassle of setting up a separate thread or dealing with data transfer. The system uses a thread pool to implement asynchronous calls (see the “Thread Pools” sidebar), and you have no way to control how many threads it assigns to processing asynchronous calls or anything else about the thread pool. Because of this, asynchronous calls aren’t suited for more than a few active calls at once.

Threads allow more flexibility than asynchronous calls but often require more design and implementation work, especially if a thread pool needs to be implemented. It’s also more work to transfer data around, and synchronization details may require more thought.

There’s also a readability issue. Thread-based code is often easier to understand (though it’s probably more likely to harbor hard-to-find problems), and it’s a more familiar idiom.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *