记录在案:是的,API 已损坏,因为 TaskCompletionSource 应接受 CancellationToken。.NET 运行时修复了这个问题以供自己使用,但在 .NET 4.6 之前没有公开修复(TrySetCanceled 的重载) 。
作为任务消费者,有两个基本选项。
- 始终检查 Task.Status
- 如果请求取消,只需检查您自己的 CancellationToken 并忽略任务错误。
所以像:
object result;
try
{
result = task.Result;
}
// catch (OperationCanceledException oce) // don't rely on oce.CancellationToken
catch (Exception ex)
{
if (task.IsCancelled)
return; // or otherwise handle cancellation
// alternatively
if (cancelSource.IsCancellationRequested)
return; // or otherwise handle cancellation
LogOrHandleError(ex);
}
第一个依赖于库编写者使用 TaskCompletionSource.TrySetCanceled 而不是使用提供匹配令牌的 OperationCanceledException 执行 TrySetException。
第二个不依赖库编写者做任何“正确”的事情,除了做任何必要的事情来处理他们的代码异常。这可能无法记录错误以进行故障排除,但无论如何都无法(合理地)从内部外部代码中清理操作状态。
对于任务生产者,可以
- 尝试通过使用反射将令牌与任务取消相关联来兑现 OperationCanceledException.CancellationToken 合同。
- 使用 Continuation 将令牌与返回的任务相关联。
后者很简单,但像 Consumer 选项 2 可能会忽略任务错误(甚至在执行序列停止之前很久就将任务标记为已完成)。
两者的完整实现(包括缓存委托以避免反射)......
更新:对于 .NET 4.6 及更高版本,只需调用TaskCompletionSource.TrySetCanceled
接受.NET 的新公共重载即可CancellationToken
。当与 .NET 4.6 链接时,使用下面扩展方法的代码将自动切换到该重载(如果使用扩展方法语法进行调用)。
static class TaskCompletionSourceExtensions
{
/// <summary>
/// APPROXIMATION of properly associating a CancellationToken with a TCS
/// so that access to Task.Result following cancellation of the TCS Task
/// throws an OperationCanceledException with the proper CancellationToken.
/// </summary>
/// <remarks>
/// If the TCS Task 'RanToCompletion' or Faulted before/despite a
/// cancellation request, this may still report TaskStatus.Canceled.
/// </remarks>
/// <param name="this">The 'TCS' to 'fix'</param>
/// <param name="token">The associated CancellationToken</param>
/// <param name="LazyCancellation">
/// true to let the 'owner/runner' of the TCS complete the Task
/// (and stop executing), false to mark the returned Task as Canceled
/// while that code may still be executing.
/// </param>
public static Task<TResult> TaskWithCancellation<TResult>(
this TaskCompletionSource<TResult> @this,
CancellationToken token,
bool lazyCancellation)
{
if (lazyCancellation)
{
return @this.Task.ContinueWith(
(task) => task,
token,
TaskContinuationOptions.LazyCancellation |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap();
}
return @this.Task.ContinueWith((task) => task, token).Unwrap();
// Yep that was a one liner!
// However, LazyCancellation (or not) should be explicitly chosen!
}
/// <summary>
/// Attempts to transition the underlying Task into the Canceled state
/// and set the CancellationToken member of the associated
/// OperationCanceledException.
/// </summary>
public static bool TrySetCanceled<TResult>(
this TaskCompletionSource<TResult> @this,
CancellationToken token)
{
return TrySetCanceledCaller<TResult>.MakeCall(@this, token);
}
private static class TrySetCanceledCaller<TResult>
{
public delegate bool MethodCallerType(TaskCompletionSource<TResult> inst, CancellationToken token);
public static readonly MethodCallerType MakeCall;
static TrySetCanceledCaller()
{
var type = typeof(TaskCompletionSource<TResult>);
var method = type.GetMethod(
"TrySetCanceled",
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.NonPublic,
null,
new Type[] { typeof(CancellationToken) },
null);
MakeCall = (MethodCallerType)
Delegate.CreateDelegate(typeof(MethodCallerType), method);
}
}
}
和测试程序...
class Program
{
static void Main(string[] args)
{
//var cts = new CancellationTokenSource(6000); // To let the operation complete
var cts = new CancellationTokenSource(1000);
var ct = cts.Token;
Task<string> task = ShouldHaveBeenAsynchronous(cts.Token);
try
{
Console.WriteLine(task.Result);
}
catch (AggregateException aex)
{
foreach (var ex in aex.Flatten().InnerExceptions)
{
var oce = ex as OperationCanceledException;
if (oce != null)
{
if (oce.CancellationToken == ct)
Console.WriteLine("OK: Normal Cancellation");
else
Console.WriteLine("ERROR: Unexpected cancellation");
}
else
{
Console.WriteLine("ERROR: " + ex.Message);
}
}
}
Console.Write("Press Enter to Exit:");
Console.ReadLine();
}
static Task<string> ShouldHaveBeenAsynchronous(CancellationToken ct)
{
var tcs = new TaskCompletionSource<string>();
try
{
//throw new NotImplementedException();
ct.WaitHandle.WaitOne(5000);
ct.ThrowIfCancellationRequested();
tcs.TrySetResult("this is the result");
}
catch (OperationCanceledException ex)
{
if (ex.CancellationToken == ct)
tcs.TrySetCanceled(ct);
else
tcs.TrySetException(ex);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
return tcs.Task;
//return tcs.TaskWithCancellation(ct, false);
}
}