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 await
s 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 Task
s 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 inSynchronizationContext.Current
if any, otherwise inTaskScheduler.Current
2ContinueWith
will schedule continuations in the provided task scheduler, or inTaskScheduler.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.
When building for Release, the compiler generates structs.
When building for Debug, the compiler generates classes to allow edit-and-continue on async code.
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.
It swallows errors instead of surfacing them.
It doesn't return a
Task
so the caller will not know when theNumber
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 usesTaskScheduler.Current
rather thanTaskScheduler.Default
.ContinueWith
does not understand asynchronous delegates, so it will return an extra task wrapper that you have to unwrap (by callingUnwrap
).ContinueWith
does not have appropriate default option arguments for asynchronous code (e.g.,TaskContinuationOptions.DenyChildAttach
andTaskContinuationOptions.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
Does Java Have Something Like C#'s Ref and Out Keywords
Java Equivalents of C# String.Format() and String.Join()
What Are Major Differences Between C# and Java
Difference Between Namespace in C# and Package in Java
Does Java Have Something Similar to C# Properties
How to Read a .Net Guid into a Java Uuid
What Is the "Volatile" Keyword Used For
How to Lock a Table on Read, Using Entity Framework
How to Use the Cancellationtoken Property
How to Get Ascii Value of String in C#
How to Create an Immutable Class
How to Fast Get Hardware-Id in C#
Is There a C# Case Insensitive Equals Operator
Webclient.Downloadstring() Returns String with Peculiar Characters