C# 5 async – Limiting concurrent calls to the server on user interactions

In windows 8 everything is unashamedly async – which is a good thing for responsive apps. What’s even better is it comes with the release of C# 5 with all the syntax goodness to make async something much more palatable to deal with.

Something to remember though is that the user can go a bit nuts asking for loads of stuff at once – which although doesn’t have much of a performance impact on the client will make your sever wish it wasn’t connected to the network.  A classic example of this is holding the down button on a list to get to the bottom.  As you go down the list your selected item changes.  If you decide to update your detail view on property change then you’ll go get data for each selection changed event (if you have 100s of items and you go top to bottom that’s 100s of calls).

That’s bad enough.  But who’s saying the last request you get back is the last one you sent?  In the asynchronous world it might not be.  This can get more interesting when you’re polling for updates, who says you aren’t half way through a poll for the previous selected item?

This is not much fun because it’s a bugger to track down later on, it may not even get noticed (it’s less likely on a nice fast local network too).  Now you could lock the UI whenever you’re doing something, but that would be missing the point.

Here is the code to do this in C# 5 (tests are at the end of the post if you’re interested).

public class AsyncActionSynchronizer<TResult>
{
    readonly Func<Task<TResult>> _asyncAction;
    int _requestedUpdates;

    public AsyncActionSynchronizer(Func<Task<TResult>> asyncAction)
    {
        _asyncAction = asyncAction;
    }

    public event EventHandler<SynchroizedActionCompletedEventArgs<TResult>> UpdateCompleted;
        
    public async void Update()
    {
        await _UpdateAsync();
    }

    private async Task _UpdateAsync()
    {
        if (Interlocked.Increment(ref _requestedUpdates) > 1)
        {
            return;
        }
        var result = default(TResult);
        Exception exception = null;
        try
        {
            result = await _asyncAction();
        }
        catch (Exception ex)
        {
            exception = ex;
        }

        if (Interlocked.Exchange(ref _requestedUpdates, 0) > 1)
        {
            Update();
            return;
        }
        if (exception != null)
        {
            _OnErrored(exception);
        }
        else
        {
            _OnSuccess(result);
        }
    }

    private void _OnErrored(Exception exception)
    {
        var handler = UpdateCompleted;
        if (handler != null) handler(this, new SynchroizedActionCompletedEventArgs<TResult>(exception));
    }

    private void _OnSuccess(TResult result)
    {
        var handler = UpdateCompleted;
        if (handler != null) handler(this, new SynchroizedActionCompletedEventArgs<TResult>(result));
    }
}

public class SynchroizedActionCompletedEventArgs<T> : AsyncCompletedEventArgs
{
    private T _result;

    public SynchroizedActionCompletedEventArgs(T result) : base(null, false, null)
    {
        _result = result;
    }

    public SynchroizedActionCompletedEventArgs(Exception error) :  base(error, false, null)
    {
    }

    public T Result
    {
        get
        {
            if (Error != null)
            {
                throw Error;
            }

            return _result;
        }
    }
}

public static class AsyncActionSynchronizer
{
    public static AsyncActionSynchronizer<T> Create<T>(Func<Task<T>> asyncAction)
    {
        return new AsyncActionSynchronizer<T>(asyncAction);
    }
}

Notice that we do not return a Task on Update.  We could, but it would make things more complicated.  What do you do if you are one of the callers who is ditched?  Do we return a cancelled task?  Do we complete it straight away?  Do we wait for the update to complete then return with that result attached?  Each have their pros and cons.  But to be honest the simplest thing to do is use an event, you ask to Update and we will raise an event when the Update completes.  This works for our use case nicely, we only ever update the UI for the latest result regardless of the number of calls.  Also, our timer which is updating the screen can call Update and then wait for any Completed before resetting himself.  Would be interested in other views though, it feels like we’re mixing the old async world with the new (notice I’m using AsyncCompletedEventArgs).

You could solve this problem with RX.  We decided not to use any RX to limit the number of things someone has to know to maintain the code.  If there were 100 problems RX solved for us then we’d use it at a drop of a hat.  There is a lot to be said for carefully picking your dependencies, even if the author is Microsoft.  If there is a bug in the code below we can fix it quickly and we can understand the consequences.  That is a luxury you don’t have even with open source (unless you’re an author or contributor).

public class when_there_is_an_exception : AsyncActionSynchronizerTests
{
    Establish context = () =>
    {
        _expectedException = new Exception();
        var tcs = new TaskCompletionSource<object>();
        tcs.SetException(_expectedException);
        _taskCompletionSources.Add(tcs);
        _asyncActionSynchronizer.UpdateCompleted += (s, e) => _expectedException = e.Error;
    };

    Because of = () => _asyncActionSynchronizer.Update();

    It should_raise_an_event_containing_the_error_and_not_throw = () => _expectedException.ShouldNotBeNull();

    static Exception _expectedException;
}

public class when_updating_via_async_synchronizer : AsyncActionSynchronizerTests
{
    Establish context = () =>
    {
        _expectedResult = new object();
        _taskCompletionSources.Add(Task.Factory.CompletedTaskCompletionSource(_expectedResult));
    };

    Because of = () => _asyncActionSynchronizer.Update();
    It should_pass_result_to_update_action = () => _results.ShouldContainOnly(_expectedResult);
    static object _expectedResult;
}

public class when_multiple_updates_do_not_inter_leave : AsyncActionSynchronizerTests
{
    Establish context = () =>
    {
        _firstResult = new object();
        _secondResult = new object();

        _taskCompletionSources.Add(Task.Factory.CompletedTaskCompletionSource(_firstResult));
        _taskCompletionSources.Add(Task.Factory.CompletedTaskCompletionSource(_secondResult));
    };

    Because of = () =>
    {
        _asyncActionSynchronizer.Update();
        _asyncActionSynchronizer.Update();
    };

    It should_contain_both_results = () => _results.Count.ShouldEqual(2);
    It should_contain_results_in_correct_order = () => _results[1].ShouldEqual(_secondResult);

    static object _firstResult;
    static object _secondResult;
}

public class when_async_synchronizer_update_is_not_completed : AsyncActionSynchronizerTests
{
    Establish context = () => _taskCompletionSources.Add(new TaskCompletionSource<object>());

    It should_not_block_waiting_for_task = () => { };
    It should_not_invoke_action = () => _results.Count.ShouldEqual(0);
}

public class when_the_first_update_completes_but_second_update_is_pending : when_there_are_concurrent_updates
{
    Because of = () => _firstUpdate.SetResult(new object());

    It should_not_yet_have_a_result = () => _results.Count.ShouldEqual(0);
}

public class when_the_second_update_completes_after_the_first_update : when_there_are_concurrent_updates
{
    Because of = () =>
    {
        _firstUpdate.SetResult(_firstResult);
        _secondUpdate.SetResult(_secondResult);
    };

    It should_throw_away_first_result = () => _results.Count.ShouldEqual(1);
    It should_handle_the_second_result_second = () => _results[0].ShouldEqual(_secondResult);
    It should_have_made_two_async_calls = () => _asyncActionCallCount.ShouldEqual(2);
}

public class when_the_second_update_completes_before_the_first_update : when_there_are_concurrent_updates
{
    Because of = () =>
    {
        _secondUpdate.SetResult(_secondResult);
        _firstUpdate.SetResult(_firstResult);
    };

    It should_only_contain_the_last_result = () => _results.ShouldContainOnly(_secondResult);
}

public class when_there_are_multiple_updates_pending : when_there_are_concurrent_updates
{
    Establish context = () =>
    {
        _asyncActionSynchronizer.Update();
        _asyncActionSynchronizer.Update();
        _asyncActionSynchronizer.Update();
        _asyncActionSynchronizer.Update();
        _asyncActionSynchronizer.Update();
    };

    Because of = () =>
    {
        _firstUpdate.SetResult(_firstResult);
        _secondUpdate.SetResult(_secondResult);
    };

    It should_skip_intermediate_updates = () => _asyncActionCallCount.ShouldEqual(2);
    It should_contain_the_last_updates_result = () => _results.ShouldContainOnly(_secondResult);
}

public class when_there_are_concurrent_updates : AsyncActionSynchronizerTests
{
    Establish context = () =>
    {
        _firstResult = new object();
        _secondResult = new object();

        _firstUpdate = new TaskCompletionSource<object>();
        _taskCompletionSources.Add(_firstUpdate);

        _secondUpdate = new TaskCompletionSource<object>();
        _taskCompletionSources.Add(_secondUpdate);

        _asyncActionSynchronizer.Update();
        _asyncActionSynchronizer.Update();
    };

    protected static TaskCompletionSource<object> _firstUpdate;
    protected static TaskCompletionSource<object> _secondUpdate;
    protected static object _firstResult;
    protected static object _secondResult;
}

public class AsyncActionSynchronizerTests
{
    Establish context = () =>
    {
        _currentTask = 0;
        _asyncActionCallCount = 0;
        _taskCompletionSources = new List<TaskCompletionSource<object>>();
        _results = new List<object>();
        _asyncActionSynchronizer = AsyncActionSynchronizer.Create(_AsyncAction);
        _asyncActionSynchronizer.UpdateCompleted += (s, e) => _Update(e);
    };

    private static Task<object> _AsyncAction()
    {
        _asyncActionCallCount++;
        return _taskCompletionSources[_currentTask++].Task;
    }

    private static void _Update(SynchroizedActionCompletedEventArgs<object> e)
    {
        if (e.Error == null)
        {
            _results.Add(e.Result);
        }
    }

    protected static List<TaskCompletionSource<object>> _taskCompletionSources;
    protected static List<object> _results = new List<object>();
    protected static AsyncActionSynchronizer<object> _asyncActionSynchronizer;
    protected static int _asyncActionCallCount;
    static int _currentTask;
}
Advertisements

About Tom Peplow

C# .Net developer based in London and the South Coast
This entry was posted in Uncategorized and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s