I've found that I can't distinguish controlled/cooperative from "uncontrolled" cancellation of Tasks/delegates without checking the source behind the specific Task or delegate.
Specifically, I've always assumed that when catching an OperationCanceledException
thrown from a "lower level operation" that if the referenced token cannot be matched to the token for the current operation, then it should be interpreted as a failure/error. This is a statement from the "lower level operation" that it gave up (quit), but not because you asked it to do so.
Unfortunately, TaskCompletionSource
cannot associate a CancellationToken
as the reason for cancellation. So any Task not backed by the built in schedulers cannot communicate the reason for its cancellation and could misreport cooperative cancellation as an error.
UPDATE: As of .NET 4.6 TaskCompletionSource can associate a CancellationToken
if the new overloads for SetCanceled
or TrySetCanceled
are used.
For instance the following
public Task ShouldHaveBeenAsynchronous(Action userDelegate, CancellationToken ct)
{
var tcs = new TaskCompletionSource<object>();
try
{
userDelegate();
tcs.SetResult(null); // Indicate completion
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken == ct)
tcs.SetCanceled(); // Need to pass ct here, but can't
else
tcs.SetException(ex);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
return tcs.Task;
}
private void OtherSide()
{
var cts = new CancellationTokenSource();
var ct = cts.Token;
cts.Cancel();
Task wrappedOperation = ShouldHaveBeenAsynchronous(
() => { ct.ThrowIfCancellationRequested(); }, ct);
try
{
wrappedOperation.Wait();
}
catch (AggregateException aex)
{
foreach (var ex in aex.InnerExceptions
.OfType<OperationCanceledException>())
{
if (ex.CancellationToken == ct)
Console.WriteLine("OK: Normal Cancellation");
else
Console.WriteLine("ERROR: Unexpected cancellation");
}
}
}
will result in "ERROR: Unexpected cancellation" even though the cancellation was requested through a cancellation token distributed to all the components.
The core problem is that the TaskCompletionSource does not know about the CancellationToken, but if THE "go to" mechanism for wrapping asynchronous operations in Tasks can't track this then I don't think one can count on it ever being tracked across interface(library) boundaries.
In fact TaskCompletionSource CAN handle this, but the necessary TrySetCanceled overload is internal so only mscorlib components can use it.
So does anyone have a pattern that communicates that a cancellation has been "handled" across Task and Delegate boundaries?