我发现我无法区分受控/合作与不受控制的"取消任务/代表而不检查特定任务或代表背后的来源。
具体来说,我总是假设当从一个"较低级别的操作中捕获OperationCanceledException
"如果引用的令牌无法与当前操作的令牌匹配,则应将其解释为失败/错误。这是来自"较低级别操作的声明"它放弃了(退出),但不是因为你要求它这样做。
很遗憾,TaskCompletionSource
无法将CancellationToken
与关联理由相关联。因此,任何没有内置调度程序支持的任务都无法传达其取消的原因,并可能错误地将合作取消误报为错误。
更新:从.NET 4.6开始,TaskCompletionSource 可以关联CancellationToken
,如果 SetCanceled
的新重载或TrySetCanceled
被使用。
例如以下
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");
}
}
}
将导致&#34;错误:意外取消&#34;即使取消 被要求通过分发给所有组件的取消令牌。
核心问题是TaskCompletionSource不知道CancellationToken,但是 如果&#34;去&#34;在任务中包装异步操作的机制无法跟踪此情况 那么我不认为可以指望它可以跨接口(库)边界进行跟踪。
实际上TaskCompletionSource可以处理这个问题,但必要的TrySetCanceled重载是内部的 所以只有mscorlib组件才能使用它。
所以,任何人都有一个模式,表明取消已被处理&#34;横过 任务和代表边界?
答案 0 :(得分:2)
我发现我无法区分受控制的&#34;不受控制的&#34; 取消任务/代表而不检查如何的细节 它们已经实施。
此外,您在等待或等待任务时遇到OperationCanceledException
例外的事实并不一定意味着任务Status
为TaskStatus.Canceled
。它也可能是TaskStatus.Faulted
。
可能有一些选项可以实现您所追求的目标。我是使用ContinueWith
执行此操作并将该延续任务传递给客户端代码,而不是原始TaskCompletionSource.Task
:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication
{
public static class TaskExt
{
public static Task<TResult> TaskWithCancellation<TResult>(
this TaskCompletionSource<TResult> @this,
CancellationToken token)
{
var registration = token.Register(() => @this.TrySetCanceled());
return @this.Task.ContinueWith(
task => { registration.Dispose(); return task.Result; },
token,
TaskContinuationOptions.LazyCancellation |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
class Program
{
static async Task OtherSideAsync(Task task, CancellationToken token)
{
try
{
await task;
}
catch (OperationCanceledException ex)
{
if (token != ex.CancellationToken)
throw;
Console.WriteLine("Cancelled with the correct token");
}
}
static void Main(string[] args)
{
var cts = new CancellationTokenSource(1000); // cancel in 1s
var tcs = new TaskCompletionSource<object>();
var taskWithCancellation = tcs.TaskWithCancellation(cts.Token);
try
{
OtherSideAsync(taskWithCancellation, cts.Token).Wait();
}
catch (AggregateException ex)
{
Console.WriteLine(ex.InnerException.Message);
}
Console.ReadLine();
}
}
}
请注意使用TaskContinuationOptions.LazyCancellation
,以确保在tcs.Task
任务之前永远不会完成延续任务(通过token
请求取消时)
另请注意,如果在通过tcs.TrySetCanceled
请求取消之前调用token
,则生成的任务将处于错误状态而非取消状态(taskWithCancellation.IsFaulted == true
但taskWithCancellation.IsCancelled == false
)。如果您希望针对隐式token
和明确tcs.TrySetCanceled
取消传播取消状态,请更改TaskWithCancellation
扩展名,如下所示:
public static Task<TResult> TaskWithCancellation<TResult>(
this TaskCompletionSource<TResult> @this,
CancellationToken token)
{
var registration = token.Register(() => @this.TrySetCanceled());
return @this.Task.ContinueWith(
task => { registration.Dispose(); return task; },
token,
TaskContinuationOptions.LazyCancellation |
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default).Unwrap();
}
已更新以发表评论:
基于Task
的库API的典型设计是客户端代码向API提供取消令牌,API返回Task
,关联使用提供的令牌。然后,API的客户端代码可以在捕获取消异常时执行令牌匹配。
TaskWithCancellation
的确切目的是创建此类Task
并将其返回给客户端。 原始TaskCompletionSource.Task
永远不会暴露给客户。取消发生是因为令牌已传递给ContinueWith
,它是如何与延续任务相关联的。 OTOH,token.Register
,TrySetCanceled
和TaskContinuationOptions.LazyCancellation
仅用于确保事情以正确的顺序发生,包括注册清理。
答案 1 :(得分:1)
仅供记录:已在.NET framework 4.6及以上TaskCompletionSource.TrySetCanceled Method (CancellationToken)
中修复此问题答案 2 :(得分:0)
对于记录:是的,API已被破坏,因为TaskCompletionSource应该接受CancellationToken。 .NET运行时将其修复为自己使用,但没有在.NET 4.6 之前公开修复(TrySetCanceled的重载)。
作为任务消费者,有两个基本选项。
类似于:
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。
第二种不依赖于库编写者做任何“正确”的事情,除了做任何必要的事情来处理代码的异常。这可能无法记录错误以进行故障排除,但无论如何都无法(合理地)从内部代码中清除操作状态。
对于任务生产者,可以
后者很简单,但是像Consumer选项2可能会忽略任务错误(甚至在执行序列停止之前很久就标记了Task)。
两者的完整实现(包括避免反射的缓存委托)......
更新:对于.NET 4.6及更高版本,只需调用接受TaskCompletionSource.TrySetCanceled
的{{1}}的新公开重载即可。使用以下扩展方法的代码在与.NET 4.6链接时将自动切换到该重载(如果使用扩展方法语法进行调用)。
CancellationToken
和测试程序......
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);
}
}
}