简体   繁体   中英

Task.WhenAny - A Task was Cancelled

In the following code:

        if (await Task.WhenAny(task, Task.Delay(100)) == task) {
            success = true;
        }
        else {
            details += "Timed out on SendGrid.";
            await task.ContinueWith(s =>
            {
                LogError(s.Exception);
            }, TaskContinuationOptions.OnlyOnFaulted);
        }

I occasionally get A task was cancelled at the call to await task.ContinueWith . My goal here is to see if the task completes within 100ms - if it doesn't, I want to handle some logging (this particular task has a resource leak so I'm trying to work around it by wrapping it in a timeout). This is being pulled from the guidance here: Debugging Task.WhenAny and Push Notifications

Why is this happening, and what can I do to prevent this exception from being thrown?

This happens when your task was not able to complete in time (in 100ms) BUT was able to complete later. You run your continuation with TaskContinuationOptions.OnlyOnFaulted and such tasks got cancelled if original task did NOT faulted. You await result of ContinueWith so if your task is not faulted - your continuation is cancelled and you have an exception.

In general such way to handle timeout does not make much sense, because even after timeout is reached you still wait until original task is completed, to log an exception. I think your have to remove await before your continuation. Then code will continue on timeout, but if task will fail later - it will be recorded.

There is another issue in your code - Task.WhenAny will never throw. So this condition:

await Task.WhenAny(task, Task.Delay(100)) == task

does NOT mean success, because task might be faulted. Always check task.Status and task.Exception even if WhenAny indicates completion of your task. By the way, that answer you linked in your question mentions this.

Update: if you don't like VS warning about not awaited call - you can either disable it for this specific line, or use extension method like this:

static class TaskExtensions
{
    public static void Forget(this Task task)
    {
        // do nothing
    }
}

And then:

task.ContinueWith(s => {
    Logger.Write(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted).Forget();

There is no harm in doing this (in this particular case of course), VS just issues this warning on every potentially awaitable call which is not awaited.

Task continuations are cancelled if they do not run. Your task continuation will run OnlyOnFaulted. If it doesn't fault, then the continuation will get cancelled.

Your two choices that don't involve handling that specific exception are to either not await anything (and not know if the Task has finished running or not), or to await the original Task. Once the await has finished, the task continuation will either run or get cancelled at that point.

If you're fine with handling the cancellation, then simply catch the TaskCancelledException (or AggregateException as appropriate) on the task continuation as needed and discard the exception.

The best way, in my opinion, for this specific case (logging the exception) is to just await the original task again and handle the TaskCancelledException, no task continuation needed and fully async/await compliant.

As already mentioned by @Evk it's your Continuation that is cancelled. But I want to add that the continuation can be considered a piece of configuration and as such be set at the time the Task is produced. Consider the following:

var task = Task.Delay(500).ContinueWith(s =>
{
    LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);

if (await Task.WhenAny(task, Task.Delay(100)) == task) {
    success = true;
} else {
    details += "Timed out on SendGrid.";
}

In this approach your exception is still logged and your logic will carry on only knowing that the Task timed out. If the rest of the logic does require knowledge of an Exception in addition to a timeout then you will need to await Task again as already discussed.

Update for clarity

Original Code (1) At this stage we cannot see where task is defined.

//task is undeclared in this snippet
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
    success = true;
}
else {
    details += "Timed out on SendGrid.";
    await task.ContinueWith(s =>
    {
        LogError(s.Exception);
    }, TaskContinuationOptions.OnlyOnFaulted);
}

(2) Let's add a mock task just for example

var task = Task.Delay(500); //defines task as a Task that will complete in 500ms
if (await Task.WhenAny(task, Task.Delay(100)) == task) {
    success = true;
}
else {
    details += "Timed out on SendGrid.";
    await task.ContinueWith(s =>
    {
        LogError(s.Exception);
    }, TaskContinuationOptions.OnlyOnFaulted);
}

(3) Next, when we await task.ContinueWith we allow a TaskCancelledException to be thrown. If the await is removed the Exception can be ignored but we get a warning for a task that is not awaited. We can ignore the warning or we can recognize that the continuation can be viewed as part of the configuration of a particular Task . With that we can configure the Continuation where we create the task with the appropriate options, ie TaskContinuationOptions.OnlyOnFaulted :

//Add continuation configuration where task is created
var task = Task.Delay(500).ContinueWith(s =>
{
    LogError(s.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);

if (await Task.WhenAny(task, Task.Delay(100)) == task) {
    success = true;
} else {
    details += "Timed out on SendGrid.";
    //removed continuation from here.
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM