Michael Ray Lovett
Michael Ray Lovett

Reputation: 7040

.net c# best way to wait for async events to complete and still have a "synchronous" flow in your code

My generalized question is this: how do you write asynchronous code that is still clear and easy to follow, like a synchronous solution would be?

My experience is that if you need to make some synchronous code asynchronous, using something like BackgroundWorker, you no longer have a series of easy to follow program statements that express your overall intent and order of activities, you end up instead with a bunch of "Done" Event Handlers, each of which starts the next BackgroundWorker, producing code that's really hard to follow.

I know that's not very clear; something more concrete:

Let's say a function in my WinForms application needs to start up some amazon EC2 instances, wait for them to become running, and then wait for them to all accept an SSH connection. A synchronous solution in pseudo code might look like this:

instances StartNewInstances() {
    instances = StartInstances()
    WaitForInstancesToBecomeRunning(instances)
    WaitForInstancesToAcceptSSHConnection(instances).
    return (instances)
    }

That's nice. What is happening is very clear, and the order of program actions is very clear. No white noise to distract you from understanding the code and the flow. I'd really like to end up with code that looks like that.

But in reality, I can't have a synchronous solution .. each of those functions can run for a long time, and each needs to do things like: update the ui, monitor for time-outs being exceeded, and retry operations periodically until success or time-out. In short, each of these needs to be happening in the background so the foreground UI thread can continue on.

But if I use solutions like BackgroundWorker, it seems like I don't end up with nice easy to follow program logic like the above. Instead I might start a background worker from my UI thread to perform the first function, and then my ui thread goes back to the UI while the worker thread runs. When it finishes, its "done" event handler might start the next Background Worker. WHen it finishes, its "done" event handler might start the last BackgroundWorker, and so on. Meaning you have to "follow the trail" of the Done Event handlers in order to understand the overall program flow.

There has to be a better way that a) lets my UI thread be responsive, b) let's my async operations be able to update the ui and most importantly c) be able to express my program as series of consecutive steps (as I've shown above) so that someone can understand the resultant code

Any and all input would be greatly appreciated! Michael

Upvotes: 9

Views: 13482

Answers (5)

Codism
Codism

Reputation: 6224

In some scenario (will explain later), you can wrap the async calls to a method like the following pseudo code:

byte[] ReadTheFile() {
  var buf = new byte[1000000];
  var signal = new AutoResetEvent(false);
  proxy.BeginReadAsync(..., data => {
    data.FillBuffer(buf);
    signal.Set();
  });
  signal.WaitOne();
  return buf;
}

For the above code to work, the call back needs to be invoked from a different thread. So this depends on what you are working with. From my experience, at least Silverlight web service calls are handled in UI thread, which means the above pattern cannot be used - if the UI thread is blocked, the previous begin call even cannot be carried out. If you are working with this kind of frameworks, another way to handle multiple async calls is to move your higher level logic to a background thread and use UI thread for communication. However, this approach is a little bit over killing in most cases because it requires some boilerplate code to start and stop background thread.

Upvotes: 0

cadrell0
cadrell0

Reputation: 17307

Async/await is really the best way to go. However, if you don't want to do wait, you can try Continuation-passing-style, or CPS. To do this, you pass a delegate into the async method, which is called when processing is complete. In my opinion, this is cleaner than having all of the extra events.

That will change this method signature

Foo GetFoo(Bar bar)
{
    return new Foo(bar);
}

To

void GetFooAsync(Bar bar, Action<Foo> callback)
{
    Foo foo = new Foo(bar);
    callback(foo);
}

Then to use it, you would have

Bar bar = new Bar();
GetFooAsync(bar, GetFooAsyncCallback);
....
public void GetFooAsyncCallback(Foo foo)
{
    //work with foo
}

This gets a little tricky when GetFoo could throw an exception. The method I prefer is to chage the signature of GetFooAsync.

void GetFooAsync(Bar bar, Action<Func<Foo>> callback)
{
    Foo foo;
    try
    {
        foo = new Foo(bar);
    }
    catch(Exception ex)
    {
        callback(() => {throw ex;});
        return;
    }

    callback(() => foo);
}

Your callback method will look like this

public void GetFooAsyncCallback(Func<Foo> getFoo)
{
    try
    {
        Foo foo = getFoo();
        //work with foo
    }
    catch(Exception ex)
    {
        //handle exception
    }
}

Other methods involve giving the callback two parameters, the actual result and an exception.

void GetFooAsync(Bar bar, Action<Foo, Exception> callback);

This relies on the callback checking for an exception, which could allow it to be ignored. Other methods have two call backs, one for success, and one for failure.

void GetFooAsync(Bar bar, Action<Foo> callback, Action<Exception> error);

To me this makes the flow more complicated, and still allows the Exception to be ignored.

However, giving the callback a method that must be called to get the result forces the callback to deal with the Exception.

Upvotes: 3

Trevor Watson
Trevor Watson

Reputation: 415

When it finishes, its "done" event handler might start the next Background Worker.

This is something that I've been struggling with for a while. Basically waiting for a process to finish without locking the UI.

Instead of using a backgroundWorker to start a backgroundWorker however, you can just do all the tasks in one backgroundWorker. Inside the backgroundWorker.DoWork function, it runs synchronously on that thread. So you can have one DoWork function that processes all 3 items.

Then you have to wait on just the one BackgroundWorker.Completed and have "cleaner" code.

So you can end up with

BackgroundWorker_DoWork
  returnValue = LongFunction1
  returnValue2 = LongFunction2(returnValue)
  LongFunction3

BackgroundWorker_ProgressReported
  Common Update UI code for any of the 3 LongFunctions

BackgroundWorker_Completed
  Notify user long process is done

Upvotes: 1

Jon Skeet
Jon Skeet

Reputation: 1499800

My generalized question is this: how do you write asynchronous code that is still clear and easy to follow, like a synchronous solution would be?

You wait for C# 5. It won't be long now. async/await rocks. You've really described the feature in the above sentence... See the Visual Studio async homepage for tutorials, the language spec, downloads etc.

At the moment, there really isn't a terribly clean way - which is why the feature was required in the first place. Asynchronous code very naturally becomes a mess, especially when you consider error handling etc.

Your code would be expressed as:

async Task<List<Instance>> StartNewInstances() {
    List<Instance> instances = await StartInstancesAsync();
    await instances.ForEachAsync(x => await instance.WaitUntilRunningAsync());
    await instances.ForEachAsync(x => await instance.WaitToAcceptSSHConnectionAsync());
    return instances;
}

That's assuming a little bit of extra work, such as an extension method on IEnumerable<T> with the form

public static Task ForEachAsync<T>(this IEnumerable<T> source,
                                   Func<T, Task> taskStarter)
{
    // Stuff. It's not terribly tricky :(
}

Upvotes: 11

Chris Shain
Chris Shain

Reputation: 51319

On the off chance that you can't wait for 5 as Jon rightly suggests, I'd suggest that you look at the Task Parallel Library (part of .NET 4). It provides a lot of the plumbing around the "Do this asynchronously, and when it finishes do that" paradigm that you describe in the question. It also has solid support for error handling in the asynchronous tasks themselves.

Upvotes: 3

Related Questions