可以检测.NET库代码中的不受控制的取消吗?

时间:2014-05-15 03:37:04

标签: c# asynchronous task-parallel-library task taskcompletionsource

我发现我无法区分受控/合作与不受控制的"取消任务/代表而不检查特定任务或代表背后的来源。

具体来说,我总是假设当从一个"较低级别的操作中捕获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;横过 任务和代表边界?

3 个答案:

答案 0 :(得分:2)

  

我发现我无法区分受控制的&#34;不受控制的&#34;   取消任务/代表而不检查如何的细节   它们已经实施。

此外,您在等待或等待任务时遇到OperationCanceledException例外的事实并不一定意味着任务StatusTaskStatus.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 == truetaskWithCancellation.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.RegisterTrySetCanceledTaskContinuationOptions.LazyCancellation仅用于确保事情以正确的顺序发生,包括注册清理。

答案 1 :(得分:1)

仅供记录:已在.NET framework 4.6及以上TaskCompletionSource.TrySetCanceled Method (CancellationToken)

中修复此问题

答案 2 :(得分:0)

对于记录:是的,API已被破坏,因为TaskCompletionSource应该接受CancellationToken。 .NET运行时将其修复为自己使用,但没有在.NET 4.6 之前公开修复(TrySetCanceled的重载)

作为任务消费者,有两个基本选项。

  1. 始终检查Task.Status
  2. 只需检查您自己的CancellationToken,如果请求取消,则忽略任务错误。
  3. 类似于:

    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。

    第二种不依赖于库编写者做任何“正确”的事情,除了做任何必要的事情来处理代码的异常。这可能无法记录错误以进行故障排除,但无论如何都无法(合理地)从内部代码中清除操作状态。

    对于任务生产者,可以

    1. 尝试通过使用反射将令牌与任务取消相关联来遵守OperationCanceledException.CancellationToken合同。
    2. 使用Continuation将令牌与返回的任务相关联。
    3. 后者很简单,但是像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);
              }
          }
      }