Click here to Skip to main content
15,886,689 members
Articles / Operating Systems / Windows

The Power of the Asynchronous Programming Model as Implemented by Delegates

Rate me:
Please Sign up or sign in to vote.
3.54/5 (20 votes)
21 Jun 2006CPOL3 min read 89.2K   47   23
The Asynchronous Programming Model (APM) is implemented by Delegates, allowing you to easily invoke any method asynchronously

Preface

This article is written for Microsoft .NET 2.0 and Visual Studio 2005. For information on changes required by v.1.1 and Visual Studio 2003, please check the comments.

Asynchronous Programming Model

The Asynchronous Programming Model (APM), as implemented by delegates, consists of three parts:

  1. BeginInvoke
  2. EndInvoke
  3. Rendezvous techniques

BeginInvoke starts an algorithm, implemented via a method, on a new thread.
EndInvoke retrieves the result of that method.
The Rendezvous techniques allow you to determine when the asynchronous operation has completed.

Rendezvous Techniques

There are three different types of Rendezvous techniques you can use to retrieve the results of an asynchronous delegate invocation. The first is Wait-Till-Completion, implemented via EndInvoke. Calling this method will block the current thread until the results of the asynchronous method are available. This is the least effective method, as it virtually eliminates the benefits of APM.

C#
// a delegate for a method that takes no params and returns a string 
private delegate string StringReturningDelegate();
private void Main()
{
    // create an instance of the delegate pointing to a method 
    // that takes ten seconds to complete     
    StringReturningDelegate fd = 
        new StringReturningDelegate(MethodThatTakes10SecondsToComplete);
    // Begin invocation of this delegate     
    IAsyncResult receipt = fd.BeginInvoke(null, null);
    // Immediately call EndInvoke, which will block for, oh, say, 
    // right about ten seconds     
    string result = fd.EndInvoke(receipt);
    Console.Write(result);
    Console.Read();
}
// A method that takes 10 seconds, then returns a string 
private string MethodThatTakes10SecondsToComplete() 
    { Thread.Sleep(10000); return "Done!"; }

This code segment demonstrates Wait-Till-Completion. You can see that it offers no benefits over calling MethodThatTakes10SecondsToComplete synchronously, as the EndInvoke method will block the calling thread.

The second Rendezvous technique is called Polling. In this technique, you check a property of the IAsyncResult object is called IsCompleted. This property will return false until the async operation has completed. The following code segment demonstrates polling on the same method:

C#
// a delegate for a method that takes no params and returns a string
private delegate string StringReturningDelegate();
private void Main()
{
    // create an instance of the delegate pointing to a method 
    // that takes ten seconds to complete    
    StringReturningDelegate fd = 
        new StringReturningDelegate(MethodThatTakes10SecondsToComplete);
    // Begin invocation of this delegate                
    IAsyncResult receipt = fd.BeginInvoke(null, null);
    Console.Write("Working");
    // Poll IsCompleted until it returns true; 
    // Sleep the current thread between checks to reduce CPU usage    
    while (!receipt.IsCompleted)
    {
        Thread.Sleep(500);  // wait half a sec        
        Console.Write('.');
    }
    string result = fd.EndInvoke(receipt);
    Console.Write(result);
    Console.Read();
}
// A method that takes 10 seconds, then returns a string
private string MethodThatTakes10SecondsToComplete() 
    { Thread.Sleep(10000); return "Done!"; }

This method isn't much better. You sleep away all the extra time that you could have been productive with (like in college). If you wanted to, you could take advantage of the loop to show some kind of procedural animation to the user, thus keeping them informed and aware that your program hasn't locked up. This makes this technique a little more useful than Wait-Till-Completion.

The third, and most efficient, Rendezvous technique is Method Callback. In this technique, you pass a delegate to the BeginInvoke method that will be called when the asynchronous operation has completed. It will not block your execution, or waste any CPU cycles. You should always use this method of Rendezvous.

C#
// a delegate for a method that takes no params and returns a string
private delegate string StringReturningDelegate();  
private void Main()        
{                 
    // create an instance of the delegate pointing to a method 
    // that takes ten seconds to complete            
    StringReturningDelegate fd = 
        new StringReturningDelegate(MethodThatTakes10SecondsToComplete);     
    // Begin invocation of this delegate                 
    fd.BeginInvoke(AsyncOpComplete, null);    
    // Do tons of work here.  No, seriously.
    Console.Read();        
}
/// <summary>
/// Retrieves the results of MethodThatTakes10SecondsToComplete 
/// when called asynchronously
/// </summary>
/// <param name="receipt">The IAsyncResult receipt.</param>
private void AsyncOpComplete(IAsyncResult receipt)
{
    //  Cast to the actual object so that we can access the delegate
    AsyncResult result = (AsyncResult)receipt;
    //  retrieve the calling delegate
    StringReturningDelegate gsld = (StringReturningDelegate)result.AsyncDelegate;
    //  Retrieve our results; this is guaranteed not to block, 
    //  as the async op is complete
    string result = gsld.EndInvoke(receipt);
    //  write the result to the console
    Console.Write(result);
}
// A method that takes 10 seconds, then returns a string        
private string MethodThatTakes10SecondsToComplete() 
    { Thread.Sleep(10000); return "Done!"; }

This method allows the program to continue execution while the async operation completes on another thread. When the operation completes, the delegate will call the AsyncOpComplete method, which was passed to the delegate via the BeginInvoke method.

No Changes Required

Take note that the implementation of MethodThatTakes10SecondsToComplete has not changed. This is the major strength of the APM. You create the method you wish to call asynchronously just as you would if it were to be used synchronously. All the work required to call this method asynchronously is performed by the delegate. All you have to do is create a delegate that matches the signature of your method, and construct a method (that returns void and takes one IAsyncResult parameter) designed to be run upon completion of the async operation.

Limitations of the APM in .NET

APM via delegates is an extremely useful and agile tool that you can use to make your programs run faster and be more responsive, but with power comes responsibility. Improper usage may leak resources. For every BeginInvoke call, you must call EndInvoke to prevent this. Additionally, you must understand that asynchronous operations can be less efficient than fast, synchronous operations. Save them for I/O bound operations or compute bound operations that you know will take time.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
Will Sullivan is a full time C# programmer in Columbia, SC. He enjoys sticking his finger through his right nostril and out his left tear duct. In photoshop.

Comments and Discussions

 
GeneralMy vote of 5 Pin
stef.tsal21-Nov-11 17:55
stef.tsal21-Nov-11 17:55 
GeneralWorker Thread GUI updates [modified] Pin
Bob Sandberg26-Jun-06 5:03
Bob Sandberg26-Jun-06 5:03 
GeneralRe: Worker Thread GUI updates Pin
William Sullivan26-Jun-06 10:00
William Sullivan26-Jun-06 10:00 
I can give you a little info on the subject.

Invoke and InvokeRequired are methods inherited from System.Windows.Forms.Control. That means that you can use them on any Windows Forms control, from a lowly Label all the way up to a Form.

For performance reasons, MS decided not to make instances of controls thread safe (in fact, practically no object instances in the framework are thread safe; all static objects and methods are, however). Controls also need access to the message pump (started by Application.Run()) in order to function properly. So the initial thread that executes when your program runs (check the Main function in program.cs (.NET 2.0)), and that creates the initial form and starts the message pump, is thus referred to as the "UI Thread". It is associated with the message pump and any forms and other controls it creates.

Controls know what thread they were created on, and check to make sure that any calls that manipulate their state are run by this thread. Any attempt to manipulate that state from a thread other than the UI thread will cause the control to throw an InvalidOperationException. To allow for this cross-thread modification of the UI, the Control class offers the InvokeRequired property and the Invoke (and InvokeBegin etc.) method.

When you check InvokeRequired, the control (Form or otherwise) retrieves the window thread process ID of its parent control (this propagates up to the initial parent, I believe). It then compares this ID with the ID of the calling thread. If they do not match, this method returns true (it may return false when the parent control's handle doesn't exist, but that's a rare occurrence that is covered in the documentation). This tells you that the current thread cannot safely manipulate the control. The solution to this problem is to send a message to the UI thread telling it to execute a method for you. This is what Invoke (and its sibilings) does for you.

The Invoke methods essentially (looking at the method in Reflector leads me to keep it simple!) posts a message to the UI thread's message pump, asking it to execute the method passed to Invoke via a delegate (function pointer). When this message is read by the UI thread's message pump, the UI thread executes the method pointed to by the passed delegate, which will be run by the UI thread and is therefore free to modify the Control. Calling Invoke will block the calling thread until the UI thread has completed its task. Calling BeginInvoke will return immediately so that the calling thread can continue processing while the UI thread services the request asynchronously (not calling EndInvoke will not leak resources in this sole case, so it is safe to ignore this part of the APM).

An interesting note--calling Invoke synchronously can cause a bottleneck that will result in your program becoming nonresponsive, if you're not careful. The following code snippet will demonstrate this:
<br />
public partial class Form1 : Form<br />
{<br />
    public Form1()<br />
    {<br />
        InitializeComponent();<br />
        //  Invoke does not work without the control<br />
        //  handle first being created<br />
        while (!IsHandleCreated)<br />
        {<br />
            // force handle creation<br />
            IntPtr temp = Handle;<br />
        }<br />
        ThreadPool.QueueUserWorkItem(Foo);<br />
        ThreadPool.QueueUserWorkItem(Foo);<br />
        ThreadPool.QueueUserWorkItem(Foo);<br />
    }<br />
    public void Foo(object o)<br />
    {<br />
        while (true)<br />
        {<br />
            rtb.Invoke(new StringDelegate(UpdateDisplay), <br />
                new object[] { "Foo!\n" });<br />
        }<br />
    }<br />
    public void UpdateDisplay(string text)<br />
    {<br />
        this.rtb.Text += text;<br />
    }<br />
<br />
    public delegate void StringDelegate(string foo);<br />
}<br />


To use this code, create a standard WinForm project in C#, add a sigle RichTextBox to the form and call it 'rtb', and then paste this into the codebehind for the form. Since I know that Foo is only called from worker threads, I don't bother to check InvokeRequired. Notice that I call Handle to explicitly cause the form to create its window handle, without which Invoke will not work (this is usually not needed; the fact that I start the worker threads in the constructor necessitates this).

This code demonstrates that worker threads pounding the message pump has the same effect as running the code synchronously via the UI thread. The message pump becomes clogged by calls to run UpdateDisplay, and the worker threads get blocked while waiting for their Invokes to be serviced. In a situation like this, you would want to add Thread.Sleep to the worker threads, indicating a small amount (like 10ms) to give the CPU time to block the worker thread and allow the UI thread to process some of its messages.
QuestionAsync operation comparison question? Pin
SDragon4221-Jun-06 5:42
SDragon4221-Jun-06 5:42 
AnswerRe: Async operation comparison question? [modified] Pin
o_manolis22-Jun-06 23:30
o_manolis22-Jun-06 23:30 
GeneralRe: Async operation comparison question? Pin
William Sullivan26-Jun-06 4:27
William Sullivan26-Jun-06 4:27 
GeneralMethod Call Back Example Problem [modified] Pin
Mike DiRenzo19-Jun-06 9:21
Mike DiRenzo19-Jun-06 9:21 
GeneralRe: Method Call Back Example Problem Pin
Petru6619-Jun-06 20:58
Petru6619-Jun-06 20:58 
GeneralRe: Method Call Back Example Problem Pin
William Sullivan26-Jun-06 4:28
William Sullivan26-Jun-06 4:28 
GeneralUm... Well... Pin
Ken Hadden19-Jun-06 8:10
Ken Hadden19-Jun-06 8:10 
GeneralRe: Um... Well... Pin
William Sullivan20-Jun-06 11:30
William Sullivan20-Jun-06 11:30 
GeneralRe: Um... Well... Pin
Chris Meech21-Jun-06 3:18
Chris Meech21-Jun-06 3:18 
QuestionWorker Threads and Window Messages Pin
Chris Meech19-Jun-06 7:37
Chris Meech19-Jun-06 7:37 
AnswerRe: Worker Threads and Window Messages Pin
William Sullivan20-Jun-06 11:38
William Sullivan20-Jun-06 11:38 
GeneralRe: Worker Threads and Window Messages Pin
Chris Meech21-Jun-06 3:17
Chris Meech21-Jun-06 3:17 
GeneralDo not agree... Pin
Edgardo Menta19-Jun-06 7:00
Edgardo Menta19-Jun-06 7:00 
GeneralRe: Do not agree... [modified] Pin
William Sullivan20-Jun-06 11:50
William Sullivan20-Jun-06 11:50 
GeneralRe: Do not agree... Pin
Edgardo Menta20-Jun-06 15:18
Edgardo Menta20-Jun-06 15:18 
GeneralRe: Do not agree... Pin
William Sullivan21-Jun-06 2:46
William Sullivan21-Jun-06 2:46 
GeneralParameter to BeginInvoke... Pin
Petru6618-Jun-06 23:21
Petru6618-Jun-06 23:21 
GeneralRe: Parameter to BeginInvoke... Pin
William Sullivan20-Jun-06 11:54
William Sullivan20-Jun-06 11:54 
GeneralRe: Parameter to BeginInvoke... Pin
Petru6620-Jun-06 21:54
Petru6620-Jun-06 21:54 
GeneralRe: Parameter to BeginInvoke... Pin
William Sullivan21-Jun-06 2:48
William Sullivan21-Jun-06 2:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.