如何在 C# 中实现共享任务:的取消
本文关键字:任务 取消 共享 实现 | 更新日期: 2023-09-27 18:34:55
All,这里有一个关于在 C# 中取消 Task:s 的复杂情况的设计/最佳实践的问题。如何实现共享任务的取消?
作为一个最小的例子,让我们假设以下内容;我们有一个长期运行的、可合作的可取消操作"工作"。它接受取消令牌作为参数,如果已取消,则抛出。它对某些应用程序状态进行操作并返回一个值。其结果由两个 UI 组件独立要求。
当应用程序状态保持不变时,应缓存 Work 函数的值,如果一个计算正在进行,则新请求不应启动第二个计算,而应开始等待结果。
任何一个 UI 组件都应该能够取消其任务,而不会影响其他 UI 组件任务。
到目前为止,你和我在一起吗?
上述可以通过引入一个任务缓存来实现,该缓存将实际的工作任务包装在 TaskCompletionSources 中,然后其 Task:s 返回到 UI 组件。如果 UI 组件取消了它的任务,则它只会放弃任务完成源任务,而不会放弃基础任务。这一切都很好。UI 组件创建 CancelSource,取消请求是正常的自上而下设计,协作的 TaskCompletionSource Task 位于底部。
现在,进入真正的问题。当应用程序状态更改时该怎么办?假设让"Work"函数对状态的副本进行操作是不可行的。
一种解决方案是侦听任务缓存(或大约(中的状态更改。如果缓存具有基础任务(运行 Work 函数的任务(使用的 CancelToken,则可以取消它。然后,这可能会触发取消所有附加的 TaskCompletionSources Task:s,因此两个 UI 组件都将获得已取消的任务。这是某种自下而上的取消。
有没有首选的方法可以做到这一点?是否有一种设计模式可以在某处描述它?
自下而上的取消可以实现,但感觉有点奇怪。UI 任务是使用 CancelToken 创建的,但由于另一个(内部(CancelToken 而被取消。此外,由于令牌不同,因此不能在UI中忽略OperationCancelException-这将(最终(导致在外部Task:s终结器中引发异常。
的尝试:
// 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;
}
听起来你想要一组贪婪的任务操作 - 你有一个任务结果提供程序,然后构造一个任务集来返回第一个完成的操作,例如:
// 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
(,并允许您"取消"计算任务。
另一件事是找出一种干净的方法,让"取消"任务等待而不会受到可怕的阻塞。我认为这是一个好的开始。