Asynchronously Wait For Task≪T≫ to Complete With Timeout

Asynchronously wait for TaskT to complete with timeout

How about this:

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
// task completed within timeout
} else {
// timeout logic
}

And here's a great blog post "Crafting a Task.TimeoutAfter Method" (from MS Parallel Library team) with more info on this sort of thing.

Addition: at the request of a comment on my answer, here is an expanded solution that includes cancellation handling. Note that passing cancellation to the task and the timer means that there are multiple ways cancellation can be experienced in your code, and you should be sure to test for and be confident you properly handle all of them. Don't leave to chance various combinations and hope your computer does the right thing at runtime.

int timeout = 1000;
var task = SomeOperationAsync(cancellationToken);
if (await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)) == task)
{
// Task completed within timeout.
// Consider that the task may have faulted or been canceled.
// We re-await the task so that any exceptions/cancellation is rethrown.
await task;

}
else
{
// timeout/cancellation logic
}

Wait for a task with delay in it while also using a timeout

if I use async / await as below then the Task won't wait at all using Task.WhenAny(...) and immediately reports it as completed

Yeah, because Task.Factory.StartNew doesn't create a task from a task, it creates a task from a synchronous function. Which is why you shouldn't be using it.

There are many ways to create a task, and you actually do use one of them: your compiler. It knows how to build tasks from async functions:

async Task WaitForSynchronizedRequestHelper(ClientSynchronizedRequest request, CancellationToken  ct)
{
while (!ct.IsCancellationRequested)
{
if (request.IsCompleted) break;
await Task.Delay(100);
}
}

async Task<bool> WaitForSynchronizedRequest(ClientSynchronizedRequest request, int timeoutInMilliseconds)
{
var cts = new CancellationTokenSource();

var task = WaitForSynchronizedRequestHelper(request, cts.Token);

if (await Task.WhenAny(task, Task.Delay(timeoutInMilliseconds)) != task)
cts.Cancel();

return request.IsCompleted;
}

Note that I replaced your patchwork boolean variable with a standard cancellation token.

Asynchronously wait for TaskT to complete with timeout

How about this:

int timeout = 1000;
var task = SomeOperationAsync();
if (await Task.WhenAny(task, Task.Delay(timeout)) == task) {
// task completed within timeout
} else {
// timeout logic
}

And here's a great blog post "Crafting a Task.TimeoutAfter Method" (from MS Parallel Library team) with more info on this sort of thing.

Addition: at the request of a comment on my answer, here is an expanded solution that includes cancellation handling. Note that passing cancellation to the task and the timer means that there are multiple ways cancellation can be experienced in your code, and you should be sure to test for and be confident you properly handle all of them. Don't leave to chance various combinations and hope your computer does the right thing at runtime.

int timeout = 1000;
var task = SomeOperationAsync(cancellationToken);
if (await Task.WhenAny(task, Task.Delay(timeout, cancellationToken)) == task)
{
// Task completed within timeout.
// Consider that the task may have faulted or been canceled.
// We re-await the task so that any exceptions/cancellation is rethrown.
await task;

}
else
{
// timeout/cancellation logic
}

awaiting task with timeout

Timers are inaccurate. By default their accuracy is around 15 ms. Anything lower than that will trigger in 15ms interval. Refer related answer.

Given that you have 1ms timer and 10ms timer; both are roughly equal so you get inconsistent results there.

The code you wrapped in Task.Run and claims to be working is just a coincidence. When I tried several times, results are inconsistent. It fails sometimes for the same reason mentioned.

You're better off increasing the timeout or just pass in a already completed task.

For example following test should consistently pass. Remember that your test should be consistent not brittle.

[Test]
public async Task AwaitWithTimeout_Calls_SuccessDelegate_On_Success()
{
var taskToAwait = Task.FromResult(0);

var successCalled = false;

await TaskHelper.AwaitWithTimeout(taskToAwait, 10, () => successCalled = true, ()=>{ });

Assert.IsTrue(successCalled);
}

For never ending task use TaskCompletionSource and don't set its result.

[Test]
public async Task AwaitWithTimeout_Calls_ErrorDelegate_On_NeverEndingTask()
{
var taskToAwait = new TaskCompletionSource<object>().Task;

var errorCalled = false;

await TaskHelper.AwaitWithTimeout(taskToAwait, 10, () => { }, ()=> errorCalled = true);

Assert.IsTrue(errorCalled);
}

Also I recommend you to avoid using null. You can just pass the empty delegate as a parameter. Then you don't want to have null checks scattered all over your codebase.

I'd write the helper method as:

public static async Task AwaitWithTimeout(this Task task, int timeout, Action success, Action error)
{
if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
{
success();
}
else
{
error();
}
}

Note that above method is an extension method; so you can call it with task instance.

await taskToAwait.AwaitWithTimeout(10, () => { }, ()=> errorCalled = true);//No nulls, just empty delegate

Deadlock with async Task.Run method with Wait from Synchronus method and timeout

You are deadlocking the application because you didn't use async/await or ConfigureAwait(false) but you chose to use Task.Wait and Task.Result instead.

You should know first that Task.Run captures the SynchronizationContext of the thread it is executed on. Then Task.Run runs on a new ThreadPool thread. When completed it will return to the parent thread to continue execution of the remaining code. When returning it will return to the captured SyncronizationContext. You are breaking the concept by using Task.Wait and Task.Result. Both Task members will invoke the Task synchronously, which means the parent thread will block itself until the child thread has completed. The child thread completes but the Task can't return to the captured SynchronizationContext to execute the remaining code (the code after Task.Wait), since the parent thread is still blocking itself by waiting for the task to run to completion.

Because you used Task.Wait in one place and Task.Result in another, you have created two potential deadlock situations:

Let's step through the Function() code:

public Task<ReturnsMessage> Function() {

1) Create a task and start it:

var task = Task.Run(
() =>
{
var result = SyncMethod();
return new ReturnMessage(result);
});

Important things do happen here:

Task.Run captures the current SynchronizationContext and starts execution in the background while the parent thread continues to execute. (If await would've been used here, then the remaining code that follows the await would be enqueued into a continuation queue for later execution. The intend is that the current thread can return (leave the current context) so that it doesn't need to wait and block. Remaining code will be executed by Task once the child thread has run to completion, since it was enqueued in the continuation queue before).

2) task.Wait() waits until the background thread has completed. Waiting means blocking the thread from continue to execute. The call stack is parked. This is equal to synchronization of the background thread, since there is no parallel execution anymore as the parent thread doesn't continue to execute but blocks:

 // Dangerous implementation of a timeout for the executing `task` object
if (task.Wait(delay)) {
return task;
}

Important things do happen here:

task.Wait() blocks the current thread (SynchronizationContext) waiting for the child thread to complete. The child task completes and Task tries to execute the previously enqueued remaining code from the continuation queue in the captured SynchronizationContext. But this context is blocked by the thread waiting for the child task to complete. Potential deadlock situation one.

The following remaining code will be unreachable:

var tcs = new TaskCompletionSource<ReturnMessage>();
tcs.SetCanceled();
return tcs.Task;

async and await was introduced to get rid of the blocking waits. await allows the parent thread to return and continue. The remaining code after await will be executed as continuation in the captured SynchronizationContext.

This is the fix for the first deadlock also using a proper task timeout solution that uses Task.WhenAny (not preferred):

public async Task<ReturnsMessage> FunctionAsync()
{
using (var cancellationTokenSource = new CancellationTokenSource())
{
try
{
var task = Task.Run(
() =>
{
// Check if the task needs to be cancelled
// because the timeout task ran to completion first
cancellationToken.ThrowIfCancellationRequested();

var result = SyncMethod();
return result;
}, cancellationTokenSource.Token);

int delay = 500;
Task timoutTask = Task.Delay(delay, cancellationTokenSource.Token);
Task firstCompletedTask = await Task.WhenAny(task, timoutTask);

if (firstCompletedTask == task)
{
// The 'task' has won the race, therefore
// cancel the 'timeoutTask'
cancellationTokenSource.Cancel();
return await task;
}
}
catch (OperationCanceledException)
{}

// The 'timeoutTask' has won the race, therefore
// cancel the 'task' instance
cancellationTokenSource.Cancel();

var tcs = new TaskCompletionSource<string>();
tcs.SetCanceled();
return await tcs.Task;
}
}

Or the fix for the first deadlock with an alternate and much better timeout approach using the CancellationTokenSouce timeout constructor overload (preferred):

public async Task<ReturnsMessage> FunctionAsync()
{
var timeout = 50;
using (var timeoutCancellationTokenSource = new CancellationTokenSource(timeout))
{
try
{
return await Task.Run(
() =>
{
// Check if the timeout elapsed
timeoutCancellationTokenSource.Token.ThrowIfCancellationRequested();

var result = SyncMethod();
return result;
}, timeoutCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
var tcs = new TaskCompletionSource<string>();
tcs.SetCanceled();
return await tcs.Task;
}
}
}

The second potential deadlocking code is the consumption of Function():

for (var i = 0; i < maxAttempts; i++)
{
return Function().Result;
}

From Microsoft Docs:

Accessing the property's [Task.Result] get accessor blocks the calling thread until the asynchronous operation is complete; it is equivalent to calling the Wait method.

The reason for the deadlocking is the same as explained before: a blocked SynchronizationContext that prevents the execution of the scheduled continuation.

To fix the second deadlock we can use async/await (preferred) or ConfigreAwait(false):

for (var i = 0; i < maxAttempts; i++)
{
return await FunctionAsync();
}

or ConfigreAwait(false). This approach can be used to force synchronized execution of an asynchronous method:

for (var i = 0; i < maxAttempts; i++)
{
return FunctionAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}

ConfigreAwait(false) instructs the Task to ignore the captured SynchronizationContext and to continue the execeution of the continuation queue on another ThreadPool thread which will never be the parent thread.

Timeout and stop a Task

While not create CancellationTokenSource from given timeout?

var timeout = 1000;

//DONE: don't forget to dispose CancellationTokenSource instance
using (var tokenSource = new CancellationTokenSource(timeout)) {
try {
var token = tokenSource.Token;

//TODO: May be you'll want to add .ConfigureAwait(false);
Task task = Task.Run(() => DoSomething(token), token);

await task;

// Completed
}
catch (TaskCanceledException) {
// Cancelled due to timeout

log.WriteToFile("Timeout_ ");
}
catch (Exception e) {
// Failed to complete due to e exception

Console.WriteLine("--StartThread ...there is an exception----");

//DONE: let's be nice and don't swallow the exception
throw;
}
}

Timeout for asynchronous TaskT with additional exception handling

Here is an extension method that you could use to explicitly observe the tasks that may fail while unobserved:

public static Task<T> AsObserved<T>(this Task<T> task)
{
task.ContinueWith(t => t.Exception);
return task;
}

Usage example:

var task = Task.Run(() => new EA.Repository()).AsObserved();

How to set timeout for a task, and then abort it

If you want to abort the task after 3s you need to send the token to the function. If you use Task.Delay and send in the token that will throw an exception on cancellation and abort the task.

class Program
{
static async Task Main(string[] args)
{
Console.WriteLine($"The main thread is {Thread.CurrentThread.ManagedThreadId}");
var cts = new CancellationTokenSource();
Person p = new Person { Name = "Apple" };
try
{
cts.CancelAfter(TimeSpan.FromSeconds(3));//limited to 3 seconds
await DoSth(p, cts.Token);
}
catch (Exception e)
{
Console.WriteLine(e.Message); //task was canceled
}
Console.WriteLine(cts.Token.IsCancellationRequested);
await Task.Delay(3000);
Console.ReadLine();
}

static async Task DoSth(Person p, CancellationToken ct)
{
p.Name = "Cat";
await Task.Delay(5000, ct); //Will throw on cancellation, so next row will not run if cancelled after 3s.
Console.WriteLine($"The async thread is {Thread.CurrentThread.ManagedThreadId}");
}
}

public class Person
{
public string Name { get; set; }
}


Related Topics



Leave a reply



Submit