Use an async callback with Task.ContinueWith
When chaining multiple tasks using the ContinueWith
method, your return type will be Task<T>
whereas T
is the return type of the delegate/method passed to ContinueWith
.
As the return type of an async delegate is a Task
, you will end up with a Task<Task>
and end up waiting for the async delegate to return you the Task
which is done after the first await
.
In order to correct this behaviour, you need to use the returned Task
, embedded in your Task<Task>
. Use the Unwrap
extension method to extract it.
async method callback with Task.ContinueWIth?
public async void ProcessHTMLData(string url)
{
string HTMLData = await GetHTMLDataAsync(url);
HTMLReadComplete(HTMLData);
}
or even
public async void ProcessHTMLData(string url)
{
HTMLReadComplete(await GetHTMLDataAsync(url));
}
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
.
How to use ContinueWith properly when the other task may continue running forever?
You can run another Task.Delay to wait
var task = Task.Factory.StartNew(() =>
{
//Small chance for infinite loop
});
var delayTask = Task.Delay(2000); // A wait task
Task.WhenAny(delayTask,task).ContinueWith((t) =>
{
//check which task has finished
});
But you should use a way to stop First Task Some How timeout it or use
CancellationTokenSource
Is Task.ContinueWith() guaranteed to run?
will there be a chance that callback will not be called if _Work completes very quickly?
No. Continuations passed to ContinueWith
will always be scheduled. If the task is already complete, they will be scheduled immediately. The task uses a thread-safe kind of "gate" to ensure that a continuation passed to ContinueWith
will always be scheduled; there is a race condition (of course) but it's properly handled so that the continuation is always scheduled regardless of the results of the race.
C# Chained ContinueWith Not Waiting for Previous Task to Complete
In short, you expect ContinueWith
to wait for a previously returned object. Returning an object (even a Task
) in ContinueWith
action does nothing with returned value, it does not wait for it to complete, it returns it and passes to the continuation if exists.
The following thing does happen:
- You run
SampleAsyncMethodAsync(0, scopeCount.ToString())
When it is completed, you execute the continuation 1:
return SampleAsyncMethodAsync(1, scopeCount.ToString());
and when it stumbles upon
await Task.Run
, it returns a task. I.e., it does not wait for SampleAsyncMethodAsync to complete.- Then, continuation 1 is considered to be completed, since it has returned a value (task)
- Continuation 2 is run.
If you wait for every asynchronous method manually, then it will run consequently:
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
.ContinueWith((prevTask) =>
{
SampleAsyncMethodAsync(1, scopeCount.ToString()).Wait();
})
.ContinueWith((prevTask2) =>
{
SampleAsyncMethodAsync(2, scopeCount.ToString()).Wait();
});
}
Using ContinueWith(async t => await SampleAsyncMethodAsync...
doesn't work as well, since it results into wrapped Task<Task>
result (explained well here).
Also, you can do something like:
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var c = SampleAsyncMethodAsync(0, scopeCount.ToString())
.ContinueWith((prevTask) =>
{
SampleAsyncMethodAsync(1, scopeCount.ToString())
.ContinueWith((prevTask2) =>
{
SampleAsyncMethodAsync(2, scopeCount.ToString());
});
});
}
However, it creates some sort of callback hell and looks messy.
You can use await
to make this code a little cleaner:
for (int count = 0; count < 3; count++)
{
int scopeCount = count;
var d = Task.Run(async () => {
await SampleAsyncMethodAsync(0, scopeCount.ToString());
await SampleAsyncMethodAsync(1, scopeCount.ToString());
await SampleAsyncMethodAsync(2, scopeCount.ToString());
});
}
Now, it runs 3 tasks for 3 counts, and each task will consequently run asynchronous method with number
equal to 1, 2, and 3.
How to correctly use Task.ContinueWith such that it runs on a Thread Pool thread
Yes you are correct, you can find the confirmation here.
Specifying that you want to use the default task scheduler, it will be using the ThreadPool.
The default task scheduler is based on the .NET Framework 4 thread pool, which provides work-stealing for load-balancing, thread injection/retirement for maximum throughput, and overall good performance. It should be sufficient for most scenarios.
As pointed by d.moncada there is a similar question with a very good anwer here
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.
Catch multiple Task callbacks via Continuewith
If you are trying to retrieve the results of all tasks, once they are all finished, you can simply use Task.WhenAll
. If all tasks return the same result, the Task.WhenAll(Task[] overload will return a Task. In this case, await Task.WhenAll
will return an array with the tasks' result in order:
var t1=Task.Run(()=>{...;return 1});
var t2=Task.Run(()=>{...;return 2});
var t3=Task.Run(()=>{...;return 3});
int[] results=await Task.WhenAll(t1,t2,t3);
//Check t1's result
Debug.Assert(results[0]==1);
When the tasks return different results, the Task.WhenAll(params Task[]) is called which returns only a Task. You can still access the results of each task though, using its Result
property withouth any risk of blocking:
var t1=Task.Run(()=>{...;return 1});
var t2=Task.Run(()=>{...;return "Hi"});
var t3=Task.Run(()=>{...;return new[]{1,2,3}});
await Task.WhenAll();
Debug.Assert(t2.Result=="Hi");
There's no reason to use ContinueWith
when using await
, as awaiting does roughly the same thing as using ContinueWith
- It doesn't start any asynchronous executions, it awaits without blocking for already running tasks to complete, it extracts the results and sets the synchronization context to what it was before awaiting started.
Related Topics
How to Lock/Unlock a File Across Process
Empty Bundle Using Ms Bundling
Attach Debugger to Iis Instance
Navigation Property Without Declaring Foreign Key
Auto Create Database Tables from Objects, Entity Framework
Use Decimal Values as Attribute Params in C#
Calling a SQL User-Defined Function in a Linq Query
String.Format() Giving "Input String Is Not in Correct Format"
How to Check That a Uri String Is Valid
How to Develop iOS App Using Xamarin Studio on Windows
Maxlength Attribute Not Generating Client-Side Validation Attributes
.Net Vs Java Garbage Collector
Method Overloading. Can You Overuse It