【问题标题】:How to implement cancellation of shared Task:s in C#如何在 C# 中实现取消共享任务:s
【发布时间】:2012-05-28 03:02:11
【问题描述】:

所有,这里有一个关于在 C# 中取消 Task:s 的复杂案例的设计/最佳实践的问题。如何实现共享任务的取消?

作为一个最小的例子,让我们假设以下内容;我们有一个长期运行的、可合作取消的操作“工作”。它接受一个取消标记作为参数,如果它被取消则抛出。它对某些应用程序状态进行操作并返回一个值。它的结果是两个 UI 组件独立需要的。

在应用程序状态不变的情况下,应缓存 Work 函数的值,如果正在进行一次计算,则新请求不应开始第二次计算,而是开始等待结果。

任何一个 UI 组件都应该能够取消它的任务,而不会影响其他 UI 组件的任务。

到目前为止,你和我在一起吗?

上述可以通过引入一个Task缓存来完成,该缓存将真正的Work任务包装在TaskCompletionSources中,然后将其Task:s返回给UI组件。如果 UI 组件取消它的任务,它只会放弃 TaskCompletionSource 任务,而不是底层任务。这一切都很好。 UI 组件创建 CancellationSource 和取消请求是一个正常的自上而下的设计,与底部的协作 TaskCompletionSource 任务。

现在,解决真正的问题。应用状态发生变化时怎么办?让我们假设让“工作”函数对状态副本进行操作是不可行的。

一种解决方案是监听任务缓存(或附近)中的状态变化。如果缓存有底层任务使用的 CancellationToken,即运行 Work 函数的任务,它可以取消它。这可能会触发所有附加的 TaskCompletionSources Task:s 的取消,因此两个 UI 组件都将获得 Canceled 任务。这是某种自下而上的取消。

有没有首选的方法来做到这一点?是否有一种设计模式可以在某处描述它?

自下而上的取消是可以实现的,但是感觉有点怪。 UI 任务是使用 CancellationToken 创建的,但由于另一个(内部)CancellationToken 而被取消。此外,由于令牌不相同,因此不能仅在 UI 中忽略 OperationCancelledException - 这将(最终)导致在外部 Task:s 终结器中引发异常。

【问题讨论】:

  • 很好的问题,虽然我的第一反应是“tl;dr”
  • 所以,澄清一下;您有任务结果的两个消费者,并且您只希望一个任务来计算每个应用程序状态的值;但是您希望每个消费者都能够“取消”等待任务结果吗?
  • 是的,但是状态的改变应该取消计算。

标签: c# multithreading cancellation


【解决方案1】:

听起来你想要一组贪婪的任务操作——你有一个任务结果提供者,然后构造一个任务集来返回第一个完成的操作,例如:

// Task Provider - basically, construct your first call as appropriate, and then 
//   invoke this on state change

public void OnStateChanged()
{
    if(_cts != null)
       _cts.Cancel();

    _cts = new CancellationTokenSource();
    _task = Task.Factory.StartNew(() =>
       {
           // Do Computation, checking for cts.IsCancellationRequested, etc
           return result;
       });
}

// Consumer 1

var cts = new CancellationTokenSource();
var task = Task.Factory.StartNew(() =>
  {
       var waitForResultTask = Task.Factory.StartNew(() =>
          {
              // Internally, this is invoking the task and waiting for it's value
              return MyApplicationState.GetComputedValue();
          });

       // Note this task cares about being cancelled, not the one above
       var cancelWaitTask = Task.Factory.StartNew(() =>
         {
              while(!cts.IsCancellationRequested)
                 Thread.Sleep(25);

              return someDummyValue;
         });

       Task.WaitAny(waitForResultTask, cancelWaitTask);

       if(cancelWaitTask.IsComplete)
          return "Blah"; // I cancelled waiting on the original task, even though it is still waiting for it's response
       else
          return waitForResultTask.Result;
  });

现在,我尚未对此进行全面测试,但它应该允许您通过取消令牌来“取消”等待任务(从而强制“等待”任务首先完成并点击WaitAny),并允许您“取消”计算任务。

另一件事是找出一种让“取消”任务等待而不会出现可怕阻塞的干净方法。我认为这是一个好的开始。

【讨论】:

  • 我使用了TaskCompletionSource 类来完成您的消费者所做的事情。但问题是一样的。当调用 OnStateChange 并取消“真正的”任务时,MyApplicationState.GetComputedValue() 函数将抛出 OpCanceledException。这将由 Task.WaitAny 重新抛出,然后“任务”需要特别注意不要在其终结器中爆炸。
  • 只有当你使用cts.ThrowIfCancellationIsRequested - 如果你只是检查线程中的条件,如果你只是返回一个虚拟值就不会抛出异常。您自己检查令牌,而不是由框架执行。或者,您可以简单地用内部 try / catch 包装 Task.WaitAny 并通过返回“已取消”虚拟返回值(或任何适当的值)来处理那里的异常
  • 你是对的。使用特殊的返回值进行取消(而不是使用异常)是实现我想要的一种方式。我将不得不引入一个虚拟值(或一种新类型),但它似乎违背了在 .Net Tasks 框架中处理带有异常的取消的一般用途。我一直在努力寻求更整洁的解决方案,但这会奏效。谢谢。
【解决方案2】:

这是我的尝试:

// the Task for the current application state
Task<Result> _task;
// a CancellationTokenSource for the current application state
CancellationTokenSource _cts;

// called when the application state changes
void OnStateChange()
{
    // cancel the Task for the old application state
    if (_cts != null)
    {
        _cts.Cancel();
    }

    // new CancellationTokenSource for the new application state
    _cts = new CancellationTokenSource();
    // start the Task for the new application state
    _task = Task.Factory.StartNew<Result>(() => { ... }, _cts.Token);
}

// called by UI component
Task<Result> ComputeResultAsync(CancellationToken cancellationToken)
{
    var task = _task;
    if (cancellationToken.CanBeCanceled && !task.IsCompleted)
    {
        task = WrapTaskForCancellation(cancellationToken, task);
    }
    return task;
}

static Task<T> WrapTaskForCancellation<T>(
    CancellationToken cancellationToken, Task<T> task)
{
    var tcs = new TaskCompletionSource<T>();
    if (cancellationToken.IsCancellationRequested)
    {
        tcs.TrySetCanceled();
    }
    else
    {
        cancellationToken.Register(() =>
        {
            tcs.TrySetCanceled();
        });
        task.ContinueWith(antecedent =>
        {
            if (antecedent.IsFaulted)
            {
                tcs.TrySetException(antecedent.Exception.GetBaseException());
            }
            else if (antecedent.IsCanceled)
            {
                tcs.TrySetCanceled();
            }
            else
            {
                tcs.TrySetResult(antecedent.Result);
            }
        }, TaskContinuationOptions.ExecuteSynchronously);
    }
    return tcs.Task;
}

【讨论】:

  • 这就像我所做的那样。它有效,但它是好的设计吗?从 UI 的角度来看,任务被其他人取消了。使用此解决方案,如果 UI 创建额外的包装任务以避免“未观察到的异常被终结器线程重新抛出”问题,则 UI 还需要显式处理来自内部任务的取消异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多