2012-05-21 25 views
6

全部,这里是一个有关在C#中取消Task:s的复杂情况的设计/最佳实践的问题。你如何实现取消共享任务?如何在C#中实现取消共享任务:s#

作为一个简单的例子,我们假设如下;我们有一个长时间运行,合作可撤销的操作'工作'。它接受一个取消令牌作为参数,并在它被取消时抛出。它在某个应用程序状态下运行并返回一个值。其结果是两个UI组件独立需要的。

当应用程序状态不变,工作函数的值应该被缓存,如果一个计算正在进行,新的请求不应该启动第二个计算,而是开始等待结果。

UI组件的任应能抵消它的任务,而不会影响其他UI组件的任务。

到目前为止你还在我身边吗?

以上可以通过引入包裹在TaskCompletionSources实际工作任务,其任务一个任务的高速缓存来完成:■然后返回到UI组件。如果一个UI组件取消它的任务,它只会放弃TaskCompletionSource任务而不是基础任务。这很好。用户界面组件创建CancellationSource,取消请求是一个正常的自顶向下设计,配合TaskCompletionSource Task在底部。

现在,真正的问题。应用程序状态更改时该怎么办?让我们假设让'工作'功能在状态副本上运行是不可行的。

一个解决办法是听在任务缓存中的状态变化(或有大约)。如果高速缓存具有底层任务使用的CancellationToken,即运行Work功能的任务,则可以取消它。这可能会触发取消所有附加的TaskCompletionSources Task:s,因此这两个UI组件都将获取取消任务。这是一种自下而上的取消。

有没有一种首选的方法来做到这一点?是否有设计模式将其描述在某个地方?

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

+0

非常好的问题,虽然我的第一反应是 “文艺青年最爱的” – dtb

+1

所以,澄清;您有两个消费者的任务结果,并且您只需要一个任务来计算每个应用程序状态的值;但是你希望每个消费者能够“取消”等待任务结果? – Tejs

+0

是的,但是状态的改变也应该取消计算。 – 4ZM

回答

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

现在,我的天堂” t已经完全测试过了,但它应该允许你通过取消令牌来“取消”等待任务(因此迫使“等待”任务先完成并点击WaitAny),并允许您“取消”计算任务。

的另一件事情是要弄清楚使得“取消”的任务等待不可怕阻塞的清洁方式。我认为这是一个好的开始。

+0

我已经使用了TaskCompletionSource类来完成你的消费者所做的事情。但问题是一样的。当调用OnStateChange并取消“真实”任务时,MyApplicationState.GetComputedValue()函数将抛出OpCanceledException。这将由Task.WaitAny重新引发,然后'任务'需要特别小心,不要在其终结器中爆炸。 – 4ZM

+1

这只有当你使用'cts.ThrowIfCancellationIsRequested'时 - 如果你只是检查线程中的条件,如果你简单地返回一个虚拟值,不会引发异常。你自己检查令牌,而不是框架。或者,您可以简单地将'Task.WaitAny'封装为内部try/catch,并通过返回“Cancelled”虚拟返回值(或任何适当的值)来处理异常。 – Tejs

+0

您是对的。使用特殊的返回值进行取消(而不是使用例外)是实现我想要的一种方式。我将不得不引入一个虚拟值(或新类型),它似乎违背了在.Net任务框架中处理取消和异常的一般用法。我保持我的手指越过整洁的解决方案,但这会起作用。谢谢。 – 4ZM

1

这里是我的尝试:

// 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; 
} 
+0

这就像我所做的一样。它的工作原理,但它是不错的设计?从用户界面的角度来看,任务被其他人取消。使用此解决方案,如果用户界面创建附加的包装任务以避免“终结线程重新抛出未观察到的异常”问题,则UI还需要明确处理来自内部任务的取消异常。 – 4ZM