155

Can someone explain if await and ContinueWith are synonymous or not in the following example. I'm trying to use TPL for the first time and have been reading all the documentation, but don't understand the difference.

Await:

String webText = await getWebPage(uri);
await parseData(webText);

ContinueWith:

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

Is one preferred over the other in particular situations?

Spongebob Comrade
  • 1,495
  • 2
  • 17
  • 32
Harrison
  • 3,843
  • 7
  • 22
  • 49
  • 5
    If you removed the `Wait` call in the second example *then* the two snippets would be (mostly) equivalent. – Servy Sep 23 '13 at 17:28
  • 4
    possible duplicate of [Is Async await keyword equivalent to a ContinueWith lambda?](http://stackoverflow.com/questions/8767218/is-async-await-keyword-equivalent-to-a-continuewith-lambda) – Stephen Cleary Sep 23 '13 at 17:39
  • FYI : Your `getWebPage` method can't be used in both codes. In the first code it has a `Task` return type while in the second it has `string` return type. so basically your code doesn't compile. - if to be precise. – Royi Namir Jun 19 '15 at 16:01
  • "Is one preferred over the other in particular situations?" ContinueWith existed before async\await was added. After async\await came, ContinueWith should probably had been marked obsolete, for some reason it is not. – osexpert Oct 01 '20 at 09:34

2 Answers2

141

Here's the sequence of code snippets I recently used to illustrate the difference and various problems using async solves.

Suppose you have some event handler in your GUI-based application that takes a lot of time, and so you'd like to make it asynchronous. Here's the synchronous logic you start with:

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem returns a Task, that will eventually produce some result you'd like to inspect. If the current result is the one you're looking for, you update the value of some counter on the UI, and return from the method. Otherwise, you continue processing more items from LoadNextItem.

First idea for the asynchronous version: just use continuations! And let's ignore the looping part for the time being. I mean, what could possibly go wrong?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

Great, now we have a method that does not block! It crashes instead. Any updates to UI controls should happen on the UI thread, so you will need to account for that. Thankfully, there's an option to specify how continuations should be scheduled, and there's a default one for just this:

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Great, now we have a method that does not crash! It fails silently instead. Continuations are separate tasks themselves, with their status not tied to that of the antecedent task. So even if LoadNextItem faults, the caller will only see a task that has successfully completed. Okay, then just pass on the exception, if there is one:

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

Great, now this actually works. For a single item. Now, how about that looping. Turns out, a solution equivalent to the logic of the original synchronous version will look something like this:

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

Or, instead of all of the above, you can use async to do the same thing:

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

That's a lot nicer now, isn't it?

Nathan White
  • 1,082
  • 7
  • 21
pkt
  • 1,808
  • 1
  • 12
  • 16
125

In the second code, you're synchronously waiting for the continuation to complete. In the first version, the method will return to the caller as soon as it hits the first await expression which isn't already completed.

They're very similar in that they both schedule a continuation, but as soon as the control flow gets even slightly complex, await leads to much simpler code. Additionally, as noted by Servy in comments, awaiting a task will "unwrap" aggregate exceptions which usually leads to simpler error handling. Also using await will implicitly schedule the continuation in the calling context (unless you use ConfigureAwait). It's nothing that can't be done "manually", but it's a lot easier doing it with await.

I suggest you try implementing a slightly larger sequence of operations with both await and Task.ContinueWith - it can be a real eye-opener.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 3
    The error handling between the two snippets is also different; it's generally easier to work with `await` over `ContinueWith` in that regard. – Servy Sep 23 '13 at 17:29
  • @Servy: True, will add something around that. – Jon Skeet Sep 23 '13 at 17:32
  • 1
    The scheduling is also quite different, i.e., what context `parseData` executes in. – Stephen Cleary Sep 23 '13 at 17:40
  • 1
    When you say **using await will implicitly schedule the continuation in the calling context**, can you explain the benefit of that and what happens in the other situation? – Harrison Sep 23 '13 at 19:52
  • 5
    @Harrison: Imagine you're writing a WinForms app - if you write an async method, by default all of the code within the method will run in the UI thread, because the continuation will be scheduled there. If you don't specify where you want the continuation to run, I don't know what the default is but it could easily end up running on a thread pool thread... at which point you can't access the UI, etc. – Jon Skeet Sep 23 '13 at 19:59
  • @JonSkeet Correct me if I'm wrong but another difference is that even without the `continue.Wait();` — _while downloading_ - in the first code - there is NO thread that seats and waits for download to complete , while in the second code - there is a thread (`webText.Start();`) that seats and wait for download to complete....no ? – Royi Namir Jun 19 '15 at 08:33
  • @Roy: It's hard to say for sure without seeing getWebPage, which appears to change what it does depending on the version. But yes, potentially... – Jon Skeet Jun 19 '15 at 08:46
  • @JonSkeet So if the method (`getWebPage(uri)`) would've been replace by `httpClient.GetAsync(uri)` (for example) - my assumption would be correct ? – Royi Namir Jun 19 '15 at 09:15
  • @Roy: No, because then it wouldn't compile for the second case. Basically the example won't compile for a single getWebPage method... We'd need a real complete example to reason about it for sure. – Jon Skeet Jun 19 '15 at 09:32
  • :-) You're right (like always). There can't be a single `getWebPage` in OP's example . because his implementation requires _different_ return types. someone should tell him his code doesn't compile. Not to mention that _if he had_ another suitable method — _then_ it would be appropriate to mention the "waiting thread while loading" difference. – Royi Namir Jun 19 '15 at 10:24
  • @JonSkeet "In the second code, you're synchronously waiting for the continuation to complete." - is that only because of `continue.Wait();`? – David Klempfner Sep 14 '21 at 10:56
  • 1
    @DavidKlempfner: Yes. The use of `Result` is okay (in this particular case) because it's only called on a task which has completed (potentially by faulting). – Jon Skeet Sep 14 '21 at 11:43