C# 可以从.NET库代码中检测到不受控制的取消吗?
我发现,如果不检查特定任务或委托背后的来源,我无法区分任务/委托的受控/合作取消和“非受控”取消 具体地说,我一直认为,当捕获从“低级操作”抛出的C# 可以从.NET库代码中检测到不受控制的取消吗?,c#,asynchronous,task-parallel-library,task,taskcompletionsource,C#,Asynchronous,Task Parallel Library,Task,Taskcompletionsource,我发现,如果不检查特定任务或委托背后的来源,我无法区分任务/委托的受控/合作取消和“非受控”取消 具体地说,我一直认为,当捕获从“低级操作”抛出的OperationCanceledException时,如果引用的令牌无法与当前操作的令牌匹配,则应将其解释为失败/错误。这是“低级操作”中的一条语句,它放弃了(退出),但不是因为您要求它这样做 不幸的是,TaskCompletionSource无法将CancellationToken关联为取消原因。因此,任何没有内置调度器支持的任务都无法告知其取消的
OperationCanceledException
时,如果引用的令牌无法与当前操作的令牌匹配,则应将其解释为失败/错误。这是“低级操作”中的一条语句,它放弃了(退出),但不是因为您要求它这样做
不幸的是,TaskCompletionSource
无法将CancellationToken
关联为取消原因。因此,任何没有内置调度器支持的任务都无法告知其取消的原因,并且可能会将协作取消误报为错误
更新:自.NET 4.6 TaskCompletionSource起,如果使用了SetCanceled
或TrySetCanceled
的新重载,则可以关联CancellationToken
例如
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");
}
}
}
公共任务应保持同步(Action userDelegate、CancellationToken ct)
{
var tcs=new TaskCompletionSource();
尝试
{
userDelegate();
SetResult(null);//表示完成
}
捕捉(操作取消例外)
{
如果(例如CancellationToken==ct)
tcs.setCancelled();//需要在此处传递ct,但无法
其他的
tcs.SetException(ex);
}
捕获(例外情况除外)
{
tcs.SetException(ex);
}
返回tcs.Task;
}
私有void OtherSide()
{
var cts=新的CancellationTokenSource();
var ct=cts.Token;
cts.Cancel();
任务包装操作=应保持同步(
()=>{ct.ThrowIfCancellationRequested();},ct);
尝试
{
wrappedOperation.Wait();
}
捕获(聚合异常aex)
{
foreach(aex.InnerExceptions中的var ex
.OfType())
{
如果(例如CancellationToken==ct)
控制台写入线(“确定:正常取消”);
其他的
Console.WriteLine(“错误:意外取消”);
}
}
}
将导致“错误:意外取消”,即使取消
通过分发给所有组件的取消令牌请求
核心问题是TaskCompletionSource不知道CancellationToken,但是
如果在任务中包装异步操作的“转到”机制无法跟踪此情况
那么,我认为人们不能指望它会被跨接口(库)边界跟踪
事实上,TaskCompletionSource可以处理这个问题,但必需的TrySetCanceled重载是内部的
因此,只有mscorlib组件可以使用它
那么,是否有人有一种模式,可以传达取消已经在整个系统中“处理”了
任务和委托边界
我发现我无法区分受控和“不受控”
取消任务/委托,但不检查如何取消的详细信息
它们得到了实施
此外,您在等待或等待任务时捕获了OperationCanceledException
异常这一事实并不一定意味着任务的状态为TaskStatus.Cancelled
。它也可能是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();
}
}
}
更新了以处理评论:
基于任务
的库API的典型设计是,客户端代码向API提供取消令牌,API返回与所提供令牌关联的任务
。然后,API的客户端代码可以在捕获取消异常时进行令牌匹配
带有取消功能的任务的确切目的是创建此类任务
,并将其返回给客户端原始的TaskCompletionSource.Task
从不向客户端公开。取消之所以发生,是因为令牌通过
传递给了ContinueWith,这就是它与continuence任务关联的方式。OTOH、token.Register
、TrySetCanceled
和TaskContinuationOptions.LazyCancellation
仅用于确保事情按正确的顺序发生,包括注册清理 对于记录:是的,API在TaskCompletionSource应接受CancellationToken的情况下被破坏。.NET运行时修复了此问题供自己使用,但没有公开.NET 4.6之前的修复(TrySetCanceled重载)
作为任务使用者,有两个基本选项
始终检查任务状态
只需检查您自己的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.TrySetConceled,而不是使用OperationCanceledException提供匹配令牌来执行TrySetException
第二种方法不依赖于库作者“正确”地做任何事情,而只是做任何必要的事情来处理代码中的异常。这可能无法记录错误以进行故障排除,但无论如何都无法(合理地)从外部代码内部清理操作状态
对于任务生产者,可以
通过使用反射将令牌与任务取消关联,尝试遵守OperationCanceledException.CancellationToken契约
使用延续将令牌与返回的任务相关联
后者很简单,但与消费者一样,选项2可能会忽略任务错误(甚至是ma)
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);
}
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);
}
}