Tuesday, April 17, 2012

C# Async pitfalls

The new features of C# is async. The powerful async.

From the samples:
public async void AsyncIntroParallel()
{
    Task<string> page1 = new WebClient().DownloadStringTaskAsync(new Uri("http://www.weather.gov"));
    Task<string> page2 = new WebClient().DownloadStringTaskAsync(new Uri("http://www.weather.gov/climate/"));
    Task<string> page3 = new WebClient().DownloadStringTaskAsync(new Uri("http://www.weather.gov/rss/"));
 
    WriteLinePageTitle(await page1);
    WriteLinePageTitle(await page2);
    WriteLinePageTitle(await page3);
}
Now, what if the task page1 fails with exception? You'll have it in WriteLinePageTitle(await page1).

Now, what if the task page2 fails with exception and WriteLinePageTitle(await page1) fails with exception? Kiss good-bye, the exception from page2 and the result from page3 are gone.

Update: in my tests, tasks page2 and page3 got killed. Very weird. Please check for yourself and download the testing solution.

Another case:
public async void ShowFileLength()
{
    Task<long> size = GetFileLengthAsync(Filename);

    labelLength.Text = (await size).ToString();
}
Async is a compiler's feature. Instead of these two lines of code there will be a big-yet-efficient state machine.

If the user closes the application before GetFileLengthAsync is ready, you'll have an exception trying to assign to labelLength.Text.

Update: what happens is: async relies on TPL, which in turn relies on ThreadPool, hiding dirty stuff from you. ThreadPool runs what it needs in background threads, and those don't prevent your process from being closed.

One way to solve this is to maintain a counter of async operations in progress and explicitly cancel any closing request.

----

At the end of the day, async is a wonderful feature.

These are pitfalls. A buggy code. Since the straightforward way to do things is now not the right way.

We'll see more programs that run faster and have a responsive UI and ... that fail more often.
Short fix: make guidelines. Good fix: change the API to have explicit BeginSection() and EndSection() methods, any much more.

UPDATE: These are not arguments purely against TPL. My own messaging library has the same issues. These are not arguments to use old school ThreadPool or IAsyncResult over TPL.

What I think asynchronous code should look like is:
public async void AsyncIntroParallel()
{
    using (var handle = AsyncManager.StartSession())
    {
        Task<string> page1 = new WebClient().DownloadStringTaskAsync(handle, new Uri("http://www.weather.gov"));
        Task<string> page2 = new WebClient().DownloadStringTaskAsync(handle, new Uri("http://www.weather.gov/climate/"));
        Task<string> page3 = new WebClient().DownloadStringTaskAsync(handle, new Uri("http://www.weather.gov/rss/"));
 
        WriteLinePageTitle(await page1);
        WriteLinePageTitle(await page2);
        WriteLinePageTitle(await page3);
    }
}

In this case, you always have all async operations under control via the handle. It should re-throw the exception (or take appropriate actions) when disposed and it's dependent async operations are not finished.

One of great `features' of .Net was that a straightforward way to do things was typically the right way to do things. This is now broken.

I still don't know a beautiful solution for the second case. See update.

Update2:

Here is a proof-of-a-concept solution: http://dl.dropbox.com/u/6656474/2012_04_26%20AsyncTests.7z

No comments:

Post a Comment