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);
        }
    }