Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I set a timeout for an Async function that doesn't accept a cancellation token?

I have my web requests handled by this code;

Response = await Client.SendAsync(Message, HttpCompletionOption.ResponseHeadersRead, CToken);

That returns after the response headers are read and before the content is finished reading. When I call this line to get the content...

return await Response.Content.ReadAsStringAsync();

I want to be able to stop it after X seconds. But it doesn't accept a cancellation token.

like image 222
iguanaman Avatar asked Sep 23 '14 01:09

iguanaman


People also ask

Should all async methods have cancellation token?

Description: Asynchronous methods should take a CancellationToken. Sometimes, async methods are not written with cooperative cancellation in mind. Either because a developer is not aware of the pattern or because they consider it unnecessary in a specific case.

What happens if an async method is not awaited?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete.

How do I stop async calls?

You can cancel an asynchronous operation after a period of time by using the CancellationTokenSource. CancelAfter method if you don't want to wait for the operation to finish.

Can I use lock inside the async method if I want to stop some operations?

Technically, yes, but it won't work as you expect. There are two reasons why thread-affine locks don't play well with async . One is that (in the general case), an async method may not resume on the same thread, so it would try to release a lock it doesn't own while the other thread holds the lock forever.


2 Answers

Have a look at How do I cancel non-cancelable async operations?. If you just want the await to finish while the request continues in the background you can use the author's WithCancellation extension method. Here it is reproduced from the article:

public static async Task<T> WithCancellation<T>( 
    this Task<T> task, CancellationToken cancellationToken) 
{ 
    var tcs = new TaskCompletionSource<bool>(); 
    using(cancellationToken.Register( 
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
        if (task != await Task.WhenAny(task, tcs.Task)) 
            throw new OperationCanceledException(cancellationToken); 
    return await task; 
}

It essentially combines the original task with a task that accepts a cancellation token and then awaits both tasks using Task.WhenAny. So when you cancel the CancellationToken the secodn task gets cancelled but the original one keeps going. As long as you don't care about that you can use this method.

You can use it like this:

return await Response.Content.ReadAsStringAsync().WithCancellation(token);

Update

You can also try to dispose of the Response as part of the cancellation.

token.Register(Reponse.Content.Dispose);
return await Response.Content.ReadAsStringAsync().WithCancellation(token);

Now as you cancel the token, the Content object will be disposed.

like image 196
NeddySpaghetti Avatar answered Oct 13 '22 02:10

NeddySpaghetti


While you can rely on WithCancellation for reuse purposes, a simpler solution for a timeout (which doesn't throw OperationCanceledException) would be to create a timeout task with Task.Delay and wait for the first task to complete using Task.WhenAny:

public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, timeoutTask).Unwrap();
}

Or, if you want to throw an exception in case there's a timeout instead of just returning the default value (i.e. null):

public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    if (task == await Task.WhenAny(task, Task.Delay(timeout)))
    {
        return await task;
    }
    throw new TimeoutException();
}

And the usage would be:

var content = await Response.Content.ReadAsStringAsync().WithTimeout(TimeSpan.FromSeconds(1));
like image 30
i3arnon Avatar answered Oct 13 '22 03:10

i3arnon