views:

964

answers:

2

Some APIs, like the WebClient, use the Event-based Async pattern. While this looks simple, and probably works well in a loosely coupled app (say, BackgroundWorker in a UI), it doesn't chain together very well.

For instance, here's a program that's multithreaded so the async work doesn't block. (Imagine this is going in a server app and called hundreds of times -- you don't want to block your ThreadPool threads.) We get 3 local variables ("state"), then make 2 async calls, with the result of the first feeding into the second request (so they can't go parallel). State could mutate too (easy to add).

Using WebClient, things end up like the following (or you end up creating a bunch of objects to act like closures):

using System;
using System.Net;

class Program
{
    static void onEx(Exception ex) {
        Console.WriteLine(ex.ToString());
    }

    static void Main() {
        var url1 = new Uri(Console.ReadLine());
        var url2 = new Uri(Console.ReadLine());
        var someData = Console.ReadLine();

        var webThingy = new WebClient();
        DownloadDataCompletedEventHandler first = null;
        webThingy.DownloadDataCompleted += first = (o, res1) => {
            if (res1.Error != null) {
                onEx(res1.Error);
                return;
            }
            webThingy.DownloadDataCompleted -= first;
            webThingy.DownloadDataCompleted += (o2, res2) => {
                if (res2.Error != null) {
                    onEx(res2.Error);
                    return;
                }
                try {
                    Console.WriteLine(someData + res2.Result);
                } catch (Exception ex) { onEx(ex); }
            };
            try {
                webThingy.DownloadDataAsync(new Uri(url2.ToString() + "?data=" + res1.Result));
            } catch (Exception ex) { onEx(ex); }
        };
        try {
            webThingy.DownloadDataAsync(url1);
        } catch (Exception ex) { onEx(ex); }

        Console.WriteLine("Keeping process alive");
        Console.ReadLine();
    }

}

Is there an generic way to refactor this event-based async pattern? (I.e. not have to write detailed extension methods for each API thats like this?) BeginXXX and EndXXX make it easy, but this event way doesn't seem to offer any way.

+3  A: 

In the past I've implemented this using an iterator method: every time you want another URL requested, you use "yield return" to pass control back to the main program. Once the request finishes, the main program calls back into your iterator to execute the next piece of work.

You're effectively using the C# compiler to write a state machine for you. The advantage is that you can write normal-looking C# code in the iterator method to drive the whole thing.

using System;
using System.Collections.Generic;
using System.Net;

class Program
{
    static void onEx(Exception ex) {
        Console.WriteLine(ex.ToString());
    }

    static IEnumerable<Uri> Downloader(Func<DownloadDataCompletedEventArgs> getLastResult) {
     Uri url1 = new Uri(Console.ReadLine());
     Uri url2 = new Uri(Console.ReadLine());
     string someData = Console.ReadLine();
     yield return url1;

     DownloadDataCompletedEventArgs res1 = getLastResult();
     yield return new Uri(url2.ToString() + "?data=" + res1.Result);

     DownloadDataCompletedEventArgs res2 = getLastResult();
     Console.WriteLine(someData + res2.Result);
    }

    static void StartNextRequest(WebClient webThingy, IEnumerator<Uri> enumerator) {
     if (enumerator.MoveNext()) {
      Uri uri = enumerator.Current;

      try {
       Console.WriteLine("Requesting {0}", uri);
       webThingy.DownloadDataAsync(uri);
      } catch (Exception ex) { onEx(ex); }
     }
     else
      Console.WriteLine("Finished");
    }

    static void Main() {
     DownloadDataCompletedEventArgs lastResult = null;
        Func<DownloadDataCompletedEventArgs> getLastResult = delegate { return lastResult; };
     IEnumerable<Uri> enumerable = Downloader(getLastResult);
     using (IEnumerator<Uri> enumerator = enumerable.GetEnumerator())
     {
      WebClient webThingy = new WebClient();
      webThingy.DownloadDataCompleted += delegate(object sender, DownloadDataCompletedEventArgs e) {
       if (e.Error == null) {
        lastResult = e;
        StartNextRequest(webThingy, enumerator);
       }
       else
        onEx(e.Error);
      };

      StartNextRequest(webThingy, enumerator);
     }

        Console.WriteLine("Keeping process alive");
        Console.ReadLine();
    }
}
Tim Robinson
+1  A: 

You might want to look into F#. F# can automate this coding for you with its «workflow» feature. The '08 PDC presentation of F# dealt with asynchronous web requests using a standard library workflow called async, which handles the BeginXXX/EndXXX pattern, but you can write a workflow for the event pattern without much difficulty, or find a canned one. And F# works well with C#.

Anton Tykhyy
To elaborate a bit on Anton's answer. The term Asynchronous Workflows is what you search for see http://blogs.msdn.com/dsyme/archive/2007/10/11/introducing-f-asynchronous-workflows.aspx
Bas Bossink
Oh yes, F# makes this a piece of cake :). But it only works with BeginXXX/EndXXX. I'm trynna figure out why so many APIs are pushing the "event based" async :(.
MichaelGG
Thanks Bas, I forgot the term «workflow».
Anton Tykhyy