Search code examples
c#.nettask-parallel-librarytask

Why would you want to use ContinueWith instead of simply appending your continuation code to the end of the background task?


The MSDN documentation for Task.ContinueWith has only one code example where one task (dTask) runs in the background, followed by (using ContinueWith) a second task (dTask2). The essence of the sample is shown below:

Task dTask = Task.Factory.StartNew(() =>
{
    //... first task code here ...
}); 

Task dTask2 = dTask.ContinueWith( (continuation) =>
{
    //... second task code here ...
});
                 
Task.WaitAll(new Task[] { dTask, dTask2 });

What is the advantage of calling the second block of code using .ContinueWith instead of appending it to the first code block, which already runs in the background and changing the code to something like this?

Task dTask = Task.Factory.StartNew( () =>
{
    //... first task code here ...
    if (!cancelled) //and,or other exception checking wrapping etc
    {
        //... second task code here ...
    }
}); 

Task.Wait(dTask);

In the suggested revision avoiding calling ContinueWith altogether, the second block of code still runs in the background, plus there's no context switching for the code to get access to closure's state... I don't get it?

For example, ContinueWith a Task on the Main thread gives an example of ContinueWith and asks an interesting question:

How exactly is it determined when the callback method will execute?

I may be wrong, but it seems to me that for the most common usages, simply appending the continuation code makes it 100% clear when the code will be "scheduled"(executed).

In the case of appending the code, it will execute "immediately" after the line above completes, and in the case of ContinueWith, well..."it depends", i.e. you need to know the internals of the Task class libraries and what default settings and schedulers are used. So, that's obviously a massive trade-off, and all the examples offered up so far don't explain WHY or WHEN you would be prepared to make this trade off? If it is indeed a trade off, and not a misunderstanding of ContinueWith's intended usage...

Here's an extract from the SO question I referenced above:

// Consider this code:
var task = Task.Factory.StartNew(() => Whatever());  
task.ContinueWith(Callback), TaskScheduler.FromCurrentSynchronizationContext())
// How exactly is it determined when the callback method will execute? 

In the spirit of learning and exploring more about ContinueWith could the above code safely be written as:

var task = Task.Factory.StartNew(() =>
{ 
    Whatever();
    Callback();
);  

...and if not, then perhaps the reason why not might lead us toward answering the question with some clarity, i.e. an example showing that the alternative would have to be written as x which would be less readable, less safe, more testable?, less ?? than using .ContinueWith.

Of course, if anyone can think of a simple real life scenario where ContinueWith provides real benefit, then that would win first prize as it would mean it would be much easier to remember it correctly.


Solution

  • The main reason for continuations is composition and asynchronous code flows.

    Composition has died a bit since "mainstream" OOP started, but as C# adopts more and more functional-programming practices (and features), it's also starting to be much more friendly to composition. Why? It allows you to reason about code easily, especially when asynchronicity is involved. Just as importantly, it allows you to very easily abstract away how exactly something is executed, which is again quite crucial when handling asynchronous code.

    Let's say you need to download a string from some web service, and use that to download another string based on that data.

    In old-school, non-asynchronous (and bad) applications, this can look something like this:

    public void btnDo_Click(object sender, EventArgs e)
    {
      var request = WebRequest.Create(tbxUrl.Text);
      var newUrl = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();
    
      request = WebRequest.Create(newUrl);
      var data = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();
    
      lblData.Text = data;
    }
    

    (error handling and proper disposal omitted :))

    This is all fine and dandy, but it has a bit of a problem in blocking the UI thread, thus making your application unresponsive for the duration of the two requests. Now, the typical solution to this was to use something like BackgroundWorker to delegate this work to a background thread, while keeping the UI responsive. Of course, this brings two issues - one, you need to make sure the background thread never accesses any UI (in our case, tbxUrl and lblData), and two, it's kind of a waste - we're using a thread just to block and wait for an asynchronous operation to complete.

    A technically better choice would be to use the asynchronous APIs. However, those are very tricky to use - a simplified example might look something like this:

    void btnDo_Click(object sender, EventArgs e)
    {
      var request = WebRequest.Create(tbxUrl.Text);
      request.BeginGetResponse(FirstCallback, request);
    
      var newUrl = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();
    
      request = WebRequest.Create(newUrl);
      var data = new StreamReader(request.GetResponse().GetResponseStream()).ReadToEnd();
    
      lblData.Text = data;
    }
    
    void FirstCallback(IAsyncResult result)
    {
      var response = ((WebRequest)result.AsyncState).EndGetResponse(result);
    
      var newUrl = new StreamReader(response.GetResponseStream()).ReadToEnd();
    
      var request = WebRequest.Create(newUrl);
      request.BeginGetResponse(SecondCallback, request);
    }
    
    void SecondCallback(IAsyncResult result)
    {
      var response = ((WebRequest)result.AsyncState).EndGetResponse(result);
    
      var data = new StreamReader(response.GetResponseStream()).ReadToEnd();
    
      BeginInvoke((Action<object>)UpdateUI, data);
    }
    
    void UpdateUI(object data)
    {
      lblData.Text = (string)data;
    }
    

    Oh, wow. Now you can see why everyone just started up a new thread instead of mucking with asynchronous code, eh? And note that this is with no error-handling whatsoever. Can you imagine how a proper reliable code must have looked like? It wasn't pretty, and most people just never bothered.

    But then the Task came with .NET 4.0. Basically, this enabled a whole new way of handling asynchronous operations, heavily inspired by functional programming (if you're interested, Task is basically a comonad). Along with an improved compiler, this allowed rewriting the whole code above into something like this:

    void btnDoAsync_Click(object sender, EventArgs e)
    {
      var request = WebRequest.Create(tbxUrl.Text);
    
      request
      .GetResponseAsync()
      .ContinueWith
      (
        t => 
          WebRequest.Create(new StreamReader(t.Result.GetResponseStream()).ReadToEnd())
          .GetResponseAsync(),
        TaskScheduler.Default
      )
      .Unwrap()
      .ContinueWith
      (
        t =>
        {
          lblData.Text = new StreamReader(t.Result.GetResponseStream()).ReadToEnd();
        },
        TaskScheduler.FromCurrentSynchronizationContext()
      );
    }
    

    The cool thing about this is that we basically still have something that looks like synchronous code - we just have to add ContinueWith(...).Unwrap() everywhere there's an asynchronous call. Adding error handling is mostly just adding another ContinueWith with TaskContinuationOptions.OnlyOnFaulted. And of course, we're chaining tasks which are basically "behaviour as a value". This means that it's very easy to create helper methods to do part of the heavy lifting for you - for example, a helper asynchronous method that handles reading the whole response as a string, asynchronously.

    Finally, there's not a lot of use cases for continuations in modern C#, because C# 5 added the await keyword, which allows you to go even further in pretending that asynchronous code is as simple as synchronous code. Compare the await based code to our original, synchronous example:

    async void btnDo_Click(object sender, EventArgs e)
    {
      var request = WebRequest.Create(tbxUrl.Text);
      var newUrl = new StreamReader((await request.GetResponseAsync()).GetResponseStream())
                   .ReadToEnd();
    
      request = WebRequest.Create(newUrl);
      var data = new StreamReader((await request.GetResponse()).GetResponseStream())
                 .ReadToEnd();
    
      lblData.Text = data;
    }
    

    The await "magically" handles all those asynchronous callbacks for us, leaving us with code that's pretty much exactly the same as the original synchronous code - but without requiring multi-threading or blocking the UI. The coolest part is that you can handle errors in the same way as if the method was synchronous - try, finally, catch... they all work the same as if everything was synchronous. It doesn't shield you from all the trickiness of asynchronous code (e.g. your UI code becomes re-entrant, similar to if you used Application.DoEvents), but it does a pretty good job overall :)

    It should be rather obvious that if you're writing code with C# 5+, you'll almost always use await rather than ContinueWith. Is there still a place for ContinueWith? The truth is, not a whole lot. I still use it in some simple helper functions, and it's pretty useful for logging (again, since tasks are easily composable, adding logging to an asynchronous function is just a matter of using a simple helper function).