Difference Between Await and Continuewith

Difference between await and ContinueWith

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.

How does using await differ from using ContinueWith when processing async tasks?

The async/await mechanism makes the compiler transform your code into a state machine. Your code will run synchronously until the first await that hits an awaitable that has not completed, if any.

In the Microsoft C# compiler, this state machine is a value type, which means it will have a very small cost when all awaits get completed awaitables, as it won't allocate an object, and therefore, it won't generate garbage. When any awaitable is not completed, this value type is inevitably boxed. 1

Note that this doesn't avoid allocation of Tasks if that's the type of awaitables used in the await expressions.

With ContinueWith, you only avoid allocations (other than Task) if your continuation doesn't have a closure and if you either don't use a state object or you reuse a state object as much as possible (e.g. from a pool).

Also, the continuation is called when the task is completed, creating a stack frame, it doesn't get inlined. The framework tries to avoid stack overflows, but there may be a case where it won't avoid one, such as when big arrays are stack allocated.

The way it tries to avoid this is by checking how much stack is left and, if by some internal measure the stack is considered full, it schedules the continuation to run in the task scheduler. It tries to avoid fatal stack overflow exceptions at the cost of performance.

Here is a subtle difference between async/await and ContinueWith:

  • async/await will schedule continuations in SynchronizationContext.Current if any, otherwise in TaskScheduler.Current 2

  • ContinueWith will schedule continuations in the provided task scheduler, or in TaskScheduler.Current in the overloads without the task scheduler parameter

To simulate async/await's default behavior:

.ContinueWith(continuationAction,
SynchronizationContext.Current != null ?
TaskScheduler.FromCurrentSynchronizationContext() :
TaskScheduler.Current)

To simulate async/await's behavior with Task's .ConfigureAwait(false):

.ContinueWith(continuationAction,
TaskScheduler.Default)

Things start to get complicated with loops and exception handling. Besides keeping your code readable, async/await works with any awaitable.

Your case is best handled with a mixed approach: a synchronous method that calls an asynchronous method when needed. An example of your code with this approach:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
{
string token = repository.GetTokenById(id);
if (string.IsNullOrEmpty(token))
{
return Task.FromResult(new SomeObject()
{
IsAuthorized = false
});
}
else
{
return InternalGetSomeObjectByTokenAsync(repository, token);
}
}

internal async Task<SomeObject> InternalGetSomeObjectByToken(Repository repository, string token)
{
SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
result.IsAuthorized = true;
return result;
}

In my experience, I've found very few places in application code where adding such complexity actually pays off the time to develop, review and test such approaches, whereas in library code any method can be a bottleneck.

The only case where I tend elide tasks is when a Task or Task<T> returning method simply returns the result of another asynchronous method, without itself having performed any I/O or any post-processing.

YMMV.



  1. When building for Release, the compiler generates structs.

    When building for Debug, the compiler generates classes to allow edit-and-continue on async code.

  2. Unless you use ConfigureAwait(false) or await on some awaitable that uses custom scheduling.

Difference Await and ContinueWith

I don't use async and await because in my head who will use this method will waiting for the result. Is correct?

That is not correct. While the await keyword does indeed wait for Connection.InsertAsync to complete before it calls Convert.ToInt32, the moment it starts waiting for Connection.InsertAsync it releases control back to its caller.

In other words, the caller will not be stuck waiting for Connection.InsertAsync to finish. The caller will be told "this will take a while, feel free to do something else", so it can continue.

Now, if the caller themselves was explicitly told to await the InsertAsync(TEntity) method on the same line that you call the method, then it will wait and it won't do anything else (except release control back to its caller), but that's because it was explicitly instructed to wait at that point.

To explain in code:

// I will wait for this result
var myInt = await Connection.InsertAsync(myEntity);

// I will not do this until I receive myInt
var sum = 1 + 1;
var anotherSum = 2 + 2;
var andAnotherSum = 3 + 3;

Without the await, the caller will just move on to the next command and do its work, all the way up to the point where it is finally told that it must await the task returned from InsertAsync(TEntity).

To explain in code:

// I will ask for this result but not wait for it
var myIntTask = Connection.InsertAsync(myEntity);

// I will keep myself busy doing this work
var sum = 1 + 1;
var anotherSum = 2 + 2;
var andAnotherSum = 3 + 3;

// My work is done. I hope the task is already done too.
// If not, I will have to wait for it because I can't put it off any longer.
var myInt = await myIntTask;

I've read some threads regards the difference between await and ContinueWith.

Functionally speaking, there is no difference between the two. However, the ContinueWith syntax has recently fallen out of popular favor, and the await syntax is much more favored because it reduces nesting and improves readability.

In terms of waiting, the behavior is exactly the same.

Personally, I suspect that ContinueWith is a leftover artifact from initially trying to design async methods the same way that promises in JS work, but this is just a suspicion.

Is Async await keyword equivalent to a ContinueWith lambda?

The general idea is correct - the remainder of the method is made into a continuation of sorts.

The "fast path" blog post has details on how the async/await compiler transformation works.

Differences, off the top of my head:

The await keyword also makes use of a "scheduling context" concept. The scheduling context is SynchronizationContext.Current if it exists, falling back on TaskScheduler.Current. The continuation is then run on the scheduling context. So a closer approximation would be to pass TaskScheduler.FromCurrentSynchronizationContext into ContinueWith, falling back on TaskScheduler.Current if necessary.

The actual async/await implementation is based on pattern matching; it uses an "awaitable" pattern that allows other things besides tasks to be awaited. Some examples are the WinRT asynchronous APIs, some special methods such as Yield, Rx observables, and special socket awaitables that don't hit the GC as hard. Tasks are powerful, but they're not the only awaitables.

One more minor nitpicky difference comes to mind: if the awaitable is already completed, then the async method does not actually return at that point; it continues synchronously. So it's kind of like passing TaskContinuationOptions.ExecuteSynchronously, but without the stack-related problems.

Combination await with ContinueWith

You can think of await as a language feature that replaces most uses of ContinueWith, and so to mix the two seems unnecessary.

Note that your second rewrite is not the same as the first, in two ways.

  1. It swallows errors instead of surfacing them.

  2. It doesn't return a Task so the caller will not know when the Number has been set to the final result.

ComputeNumber is of dubious purpose (this may be as a result of it being simplified for posting here). Why not simply await bar.GetNumberAsync() and then use the obtained value in subsequent statements? The point of await is to allow Task<T> to be treated much the same as T in procedural code.

Diffrence between async/await and ContinueWith

Await does use ContinueWith under the hood. However, ContinueWith is a dangerous API and should not be used directly. As I describe in my post on task continuations:

  • ContinueWith has an unfortunate choice of default sceduler; it uses TaskScheduler.Current rather than TaskScheduler.Default.
  • ContinueWith does not understand asynchronous delegates, so it will return an extra task wrapper that you have to unwrap (by calling Unwrap).
  • ContinueWith does not have appropriate default option arguments for asynchronous code (e.g., TaskContinuationOptions.DenyChildAttach and TaskContinuationOptions.ExecuteSynchronously).

For these reasons, you should use Await instead of ContinueWith for asynchronous code.

Also, it's shorter and prettier. :)

Async in Task ContinueWith behavior?

The code below is equivalent to your example, with variables explicitly declared, so that it's easier to see what's going on:

Task task = Task.Run(() => { });

Task<Task> continuation1 = task.ContinueWith(async prev =>
{
Console.WriteLine("Continue with 1 start");
await Task.Delay(1000);
Console.WriteLine("Continue with 1 end");
});

Task continuation2 = continuation1.ContinueWith(prev =>
{
Console.WriteLine("Continue with 2 start");
});

await continuation2;
Console.WriteLine($"task.IsCompleted: {task.IsCompleted}");
Console.WriteLine($"continuation1.IsCompleted: {continuation1.IsCompleted}");
Console.WriteLine($"continuation2.IsCompleted: {continuation2.IsCompleted}");

Console.WriteLine($"continuation1.Unwrap().IsCompleted:" +
$" {continuation1.Unwrap().IsCompleted}");

await await continuation1;

Output:

Continue with 1 start

Continue with 2 start

task.IsCompleted: True

continuation1.IsCompleted: True

continuation2.IsCompleted: True

continuation1.Unwrap().IsCompleted: False

Continue with 1 end

The tricky part is the variable continuation1, that is of type Task<Task>. The ContinueWith method does not unwrap automatically the Task<Task> return values like Task.Run does, so you end up with these nested tasks-of-tasks. The outer Task's job is just to create the inner Task. When the inner Task has been created (not completed!), then the outer Task has been completed. This is why the continuation2 is completed before the inner Task of the continuation1.

There is a built-in extension method Unwrap that makes it easy to unwrap a Task<Task>. An unwrapped Task is completed when both the outer and the inner tasks are completed. An alternative way to unwrap a Task<Task> is to use the await operator twice: await await.



Related Topics



Leave a reply



Submit