Use an Async Callback with Task.Continuewith

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



Leave a reply



Submit