I'm trying to wait for multiple tasks, and I expect that some might be cancelled. I'm using this question as a source of inspiration to have WhenAll handle cancelled tasks accordingly...
However, if none of the tasks ever get cancelled, this in turn throws an exception too!
var source = new CancellationTokenSource();
var task1 = Task.Delay(100, source.Token);
source.Cancel();
var task2 = Task.Delay(300);
await Task.WhenAll(task1, task2).ContinueWith(_ => { }, TaskContinuationOptions.OnlyOnCanceled);
In the example above if the source.Cancel() is NOT executed, then the final await will throw an exception. I could fix that by removing the ContinueWith from the final line, but then if a task IS cancelled, the WhenAll will fail with the same error.
What's the correct course of action to wait for a list of tasks when some of them might (but don't have to) be cancelled?
The ContinueWith is a primitive method, generally unsuitable for application code. Mixing it with await is even more inadvisable, since then you use two different mechanisms having slightly different semantics to accomplish the same goal. Your example implies that you want to ignore exceptions caused by the cancellation of the CancellationTokenSource. My advice is to just catch these exceptions and ignore them.
var cts = new CancellationTokenSource(100);
var task1 = Task.Delay(200, cts.Token);
var task2 = Task.Delay(300);
try
{
await Task.WhenAll(task1, task2);
}
catch (OperationCanceledException) { } // Ignore cancellation exceptions
You can be even more specific by using the when contextual keyword, in order to avoid ignoring exceptions originated by unknown CancellationTokenSources:
catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token) { }
This may create different problems though, because a method may wrap the supplied CancellationToken into a linked CancellationTokenSource, in which case the catch/when may not handle an OperationCanceledException that was originated by a known token.
I think the problem is the TaskContinuationOption you pass. In your example you use OnlyOnCanceled. So it only continues with that if a task was cancelled.
I'm not sure what the desired behaviour is when a task was cancelled. If you only want to proceed when none of them was cancelled, you could use NotOnCanceled. If you want to proceed in either case, with cancelled tasks or not, then you could for example use NotOnFaulted, since cancellation is not regarded as fault.
Related
When reviewing work by a contractor, I came upon the following extension method:
public static class TaskExtensions
{
public static async Task<IEnumerable<TResult>> WhenAllSynchronousExecution<TResult>(this IEnumerable<Task<TResult>> tasks, CancellationToken token = default(CancellationToken))
{
var results = new List<TResult>();
var exceptions = new List<Exception>();
foreach (var task in tasks)
{
if (task == null)
{
throw new ArgumentNullException(nameof(tasks));
}
if (token.IsCancellationRequested) {
exceptions.Add(new OperationCanceledException("Tasks collection cancelled", token));
break;
}
try
{
results.Add(await task);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
if (exceptions.Any()) {
throw new AggregateException(exceptions);
}
return results;
}
}
To this looks to be exactly what Task.WhenAll<TResult> already does. quote the remarks:
The call to WhenAll(IEnumerable<Task>) method does not block the calling thread. However, a call to the returned Result property does block the calling thread.
If any of the supplied tasks completes in a faulted state, the returned task will also complete in a Faulted state, where its exceptions will contain the aggregation of the set of unwrapped exceptions from each of the supplied tasks.
If none of the supplied tasks faulted but at least one of them was canceled, the returned task will end in the Canceled state.
If none of the tasks faulted and none of the tasks were canceled, the resulting task will end in the RanToCompletion state. The Task.Result property of the returned task will be set to an array containing all of the results of the supplied tasks in the same order as they were provided (e.g. if the input tasks array contained t1, t2, t3, the output task's Task.Result property will return an TResult[] where arr[0] == t1.Result, arr[1] == t2.Result, and arr[2] == t3.Result).
If the tasks argument contains no tasks, the returned task will immediately transition to a RanToCompletion state before it's returned to the caller. The returned TResult[] will be an array of 0 elements.
So to me it seems they've recreated an existing method. The only difference seems to be the checking of the CancellationToken, but I don't see mych added value there (it's not used in the code). And maybe that it's an extension method, but that's also not used in the code (it's called like var task = TaskExtensions.WhenAllSynchronousExecution(tasks);)
But this colleague is quite "sensitive". So I need to pick my battles. So is this just a custom,meagre recreation of Task.WhenAll<TResult>?
Nope, there are quite a lot of differences between the Task.WhenAll and the WhenAllSynchronousExecution. Here are some of them:
The Task.WhenAll materializes immediately the IEnumerable<Task<TResult>> sequence, by enumerating it and storing the tasks in an array. This ensures that all the tasks are up and running, before attaching continuations on them. The WhenAllSynchronousExecution doesn't do that, so it could potentially start each task after the completion of the previous task (sequentially), depending on how the enumerable sequence is implemented.
The Task.WhenAll doesn't take a CancellationToken, so when it completes you can be 100% sure that all the tasks have completed. It is always possible to fire-and-forget a Task.WhenAll task, just like any other Task, with the WaitAsync method (.NET 6), so there is no reason to bake this functionality in every async method.
The Task.WhenAll propagates the exceptions directly, as the InnerExceptions of the Task.Exception property. The WhenAllSynchronousExecution wraps the exceptions in an additional AggregateException. So you can't switch between the two APIs without rethinking your exception-handling code.
The Task.WhenAll completes as Canceled when none of the tasks is Faulted, and at least one is Canceled. The WhenAllSynchronousExecution propagates the cancellation as error, always.
The WhenAllSynchronousExecution captures the SynchronizationContext in its internal await points, so it might cause a deadlock if the caller blocks on async code.
The Task.WhenAll checks for null task instances synchronously. The WhenAllSynchronousExecution does the check during the loop that awaits each task.
Correction: Actually the integrated CancellationToken is a useful feature, that has been requested on GitHub.
I think the key to figuring out if this method has any reason to exist is to confirm if the tasks are started before the method is called.
If the tasks are not started then the method should probably be called ~WhenAllSequential.
If the tasks are started then this method seems not to add anything good.
In any case, I would argue against having this method as an extension method:
Having this method shown in auto-completion hints will sooner or later make someone pass already started tasks and think the tasks will be executed sequentially but it's the caller of this method who is in charge when the tasks are started.
Even the official WhenAll is not an extension method.
I encountered a strange behavior while writing some complex async/await code. I managed to create accidentally a canceled Task with a dual (schizophrenic) identity. It can either throw a TaskCanceledException or an OperationCanceledException, depending on how I wait it.
Waiting it with Wait throws an AggregateException, that contains a TaskCanceledException.
Waiting it with await throws an OperationCanceledException.
Here is a minimal example that reproduces this behavior:
var canceledToken = new CancellationToken(true);
Task task = Task.Run(() =>
{
throw new OperationCanceledException(canceledToken);
});
try { task.Wait(); } // First let's Wait synchronously the task
catch (AggregateException aex)
{
var ex = aex.InnerException;
Console.WriteLine($"task.Wait() failed, {ex.GetType().Name}: {ex.Message}");
}
try { await task; } // Now let's await the same task asynchronously
catch (Exception ex)
{
Console.WriteLine($"await task failed, {ex.GetType().Name}: {ex.Message}");
}
Console.WriteLine($"task.Status: {task.Status}");
Output:
task.Wait() failed, TaskCanceledException: A task was canceled.
await task failed, OperationCanceledException: The operation was canceled.
task.Status: Canceled
Try it on Fiddle.
Can anyone explain why is this happening?
P.S. I know that the TaskCanceledException derives from the OperationCanceledException. Still I don't like the idea of exposing an async API that demonstrates such a weird behavior.
Variants: The task below has a different behavior:
Task task = Task.Run(() =>
{
canceledToken.ThrowIfCancellationRequested();
});
This one completes in a Faulted state (instead of Canceled), and propagates an OperationCanceledException with either Wait or await.
This is quite puzzling because the CancellationToken.ThrowIfCancellationRequested method does nothing more than throwing an OperationCanceledException, according to the source code!
Also the task below demonstrates yet another different behavior:
Task task = Task.Run(() =>
{
return Task.FromCanceled(canceledToken);
});
This task completes as Canceled, and propagates a TaskCanceledException with either Wait or await.
I have no idea what's going on here!
Summarising multiple comments, but:
Task.Wait() is a legacy API that pre-dates await
for historic reasons, .Wait() would manifest cancellation as TaskCanceledException; to preserve backwards compatibility, .Wait() intervenes here, to expose all OperationCanceledException faults as TaskCanceledException, so that existing code continues to work correctly (in particular, so that existing catch (TaskCanceledException) handlers continue to work)
await uses a different API; .GetAwaiter().GetResult(), which behaves more like you would expect and want (although it is not expected to be used until the task is known to have completed), BUT!
in reality, you should almost never use .Wait() or .GetAwaiter().GetResult(), preferring await in almost all cases - see "sync over async"
if you're consuming an async API, and you choose (for whatever reason) to use .Wait() or GetAwaiter().GetResult(), then you are stepping into danger, and any consequences are now entirely your fault as the consumer; this is not something that a library author can, or should, compensate for (other than providing twin synchronous and asynchronous APIs)
in particular, note that while you might get away with subverting the awaiter API with Task[<T>], this pattern with various other awaitables would be an undefined behaviour (to be honest, I'm not sure it is really "defined" for Task[<T>])
equally: any deadlocks caused by "sync over async" (usually sync-context related) are entirely the problem of the consumer invoking a synchronous wait on an awaitable result
if in doubt: await (but equally, only await once; using await multiple times is also an undefined behaviour for awaitables other than Task[<T>])
for your exception handling: prefer catch (OperationCanceledException) over catch (TaskCanceledException), since the former will handle both via inheritance
Pls look at this code, running on .Net Core 2.0:
var src = new CancellationTokenSource(5000);
var token = src.Token;
var responseTask = await Task.Factory.StartNew(async () =>
{
//Uncomment bellow to reproduce locally
//await Task.Delay(60000);
return await BadSDK.OperationThatDoesNotReceiveCancellationToken();//takes around 1 min
}, token);
var response = await responseTask;
My issue here is that the await is always awaiting the very long sdk call, instead of waiting the 5sec.
What am I doing wrong? Where is my understanding wrong?
Edit1: This code behaves as expected:
var src = new CancellationTokenSource(5000);
var token = src.Token;
var responseTask = Task.Factory.StartNew(() =>
{
var task = BadSDK.OperationThatDoesNotReceiveCancellationToken();
task.Wait(token);
cancellationToken.ThrowIfCancellationRequested();
return task.Result;
}, token);
meaning, after 5 seconds, a exception is thrown
The problem is that the cancellation token pattern expects the task to check the token and exit or throw an error when the token has expired. Well written tasks will periodically check to see if cancellation is canceled and then the task can do any cleanup necessary and return gracefully or throw an error.
As you demonstrated, BadSDK.OperationThatDoesNotReceiveCancellationToken doesn't accept a CancellationToken and thus won't take any action based on the token. It doesn't matter if the token automatically requests cancellation via a timeout, or if the request is issued in some other matter. The simple fact is that BadSDK.OperationThatDoesNotReceiveCancellationToken simply isn't checking it.
In your Edit1, the CancellationToken is passed to Wait which does watch the token and will exit when cancellation is requested. This does not mean however that task was killed or stopped, it only stopped waiting for it. Depending on what you intend, this may or may not do what you want. While it does return after 5 seconds, the task will still continue running. You could even wait on it again. It may be possible to then kill the task but in practice, it can be a very bad thing to do (See Is it possible to abort a Task like aborting a Thread (Thread.Abort method)?)
The StartNew overload you used is a source of endless confusion. Doubly so, since its type is actually StartNew<Task<T>> (a nested task) rather than StartNew<T>.
A token on its own does nothing. Some code somewhere has to check the token, and throw an exception to exit the Task.
Official documentation follows this pattern:
var tokenSource = new CancellationTokenSource();
var ct = tokenSource.Token;
var task = Task.Run(() =>
{
while (...)
{
if (ct.IsCancellationRequested)
{
// cleanup your resources before throwing
ct.ThrowIfCancellationRequested();
}
}
}, ct); // Pass same token to Task.Run
But if you're anyway checking the token, and maybe throwing an exception why do you need to pass in the token in the first place, and then use the same token inside the closure?
The reason is that the token you pass in is what's used to move the Task to a cancelled state.
When a task instance observes an OperationCanceledException thrown by
user code, it compares the exception's token to its associated token
(the one that was passed to the API that created the Task). If they
are the same and the token's IsCancellationRequested property returns
true, the task interprets this as acknowledging cancellation and
transitions to the Canceled state.
P.S.
If you're on .Net Core or 4.5+, Task.Run is preferred to the factory approach.
You are passing the task the cancelation token token, but you don't specify what to do with the token within the async method.
You'd probably want to add token.ThrowIfCancellationRequested(); in the method, perhaps conditioned by token.IsCancellationRequested. This way the task will be canceled if src .Cancel() is called.
So, according to an answer to this post :
2) If the body of the task is also monitoring the cancellation token
and throws an OperationCanceledException containing that token (which
is what ThrowIfCancellationRequested does), then when the task sees
that OCE, it checks whether the OCE's token matches the Task's token.
If it does, that exception is viewed as an acknowledgement of
cooperative cancellation and the Task transitions to the Canceled
state (rather than the Faulted state).
From this I understood that by passing a token to the constructor of the task and then calling that same token's ThrowIfCancellationRequested() method, the task would in fact terminate peacefully, without me having to catch the OperationCanceledException explicitly.
However as it turns out, an exception is thrown, so I believe I may have misunderstood the mechanic.
My code:
public void AI()
{
IsBusy = true;
var token = stopGameCancellationTokenSource.Token;
var workTask = new Task(() => aiWork(token), token);
workTask.Start();
workTask.ContinueWith(task => { IsBusy = false; });
}
private void aiWork(CancellationToken token)
{
while ( true)
{
//Some computation being done here
token.ThrowIfCancellationRequested(); //Exception is thrown here, I thought it wouldn't
//More computation here, which I don't want to happen if cancellation has benn requested
}
}
This line
token.ThrowIfCancellationRequested();
explicitly throws an exception. What the link was telling you is that if the token of the task matches the token in the OperationCanceledException that has just been thrown, "the Task transitions to the Canceled state (rather than the Faulted state)."
So the bottom line is if you don't want an exception to be thrown when the task is canceled, simply omit that line!
In addition to the explanation in #JurgenCamilleri answer of why you are getting the error, what you likely intended to do was loop until cancellation was requested. You can do that by changing your code to something like this:
private void aiWork(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
//Some computation being done here
if (token.IsCancellationRequested)
break; // need to cancel
//More computation here, which I don't want to happen if cancellation has been requested
}
}
As the name of the method suggests, ThrowIfCancellationRequested will throw an exception (OperationCanceledException) if a cancelletion was requested.
If you really don't want an exception to be thrown, you can check if token.IsCancellationRequested is true and, in this case, exit your function.
However, I'd recommand sticking with token.ThrowIfCancellationRequested() unless you got good reasons not to.
Ok, so I understand how to do Task cancellations using CancellationTokenSource. it appears to me that the Task type "kind of" handles this exception automatically - it sets the Task's Status to Cancelled.
Now you still actually have to handle the OperationCancelledException. Otherwise the exception bubbles up to Application.UnhandledException. The Task itself kind of recognizes it and does some handling internally, but you still need to wrap the calling code in a try block to avoid the unhandled exception. Sometimes, this seems like unnecessary code. If the user presses cancel, then cancel the Task (obviously the task itself needs to handle it too). I don't feel like there needs to be any other code requirement. Simply check the Status property for the completion status of the task.
Is there any specific reason for this from a language design point of view? Is there any other way to set the Status property to cancelled?
You can set a Task's status to cancelled without a CancellationToken if you create it using TaskCompletionSource
var tcs = new TaskCompletionSource();
var task = tcs.Task;
tcs.SetCancelled();
Other than that you can only cancel a running Task with a CancellationToken
You only need to wrap the calling code in a try/catch block where you're asking for the result, or waiting for the task to complete - those are the situations in which the exception is thrown. The code creating the task won't throw that exception, for example.
It's not clear what the alternative would be - for example:
string x = await GetTaskReturningString();
Here we never have a variable referring to the task, so we can't explicitly check the status. We'd have to use:
var task = GetTaskReturningString();
string x = await task;
if (task.Status == TaskStatus.Canceled)
{
...
}
... which is not only less convenient, but also moves the handling of the "something happened" code into the middle of the normal success path.
Additionally, by handling cancellation with an exception, if you have several operations, you can put all the handling in one catch block instead of checking each task separately:
try
{
var x = await GetFirstTask();
var y = await GetSecondTask(x);
}
catch (OperationCanceledException e)
{
// We don't care which was canceled
}
The same argument applies for handling cancellation in one place wherever in the stack the first cancellation occurred - if you have a deep stack of async methods, cancellation in the deepest method will result in the top-most task being canceled, just like normal exception propagation.