【发布时间】:2019-03-12 00:56:05
【问题描述】:
使用 TPL,我们有 CancellationTokenSource,它提供令牌,对于合作取消当前任务(或其开始)很有用。
问题:
将取消请求传播到所有挂钩的正在运行的任务需要多长时间?
是否有任何地方,代码可以检查:“从现在开始”每个感兴趣的Task,会发现已请求取消?
为什么需要它?
我希望进行稳定的单元测试,以证明取消在我们的代码中有效。
问题详情:
我们有生成任务的“执行器”,这些任务包含一些长时间运行的操作。 executor 的主要工作是限制启动了多少并发操作。所有这些任务都可以单独取消,并且这些操作将在内部尊重CancellationToken。
我想提供单元测试,它表明当任务正在等待 slot 启动 给定操作 时发生取消,该任务将自行取消(最终)并且不会开始执行给定的操作。
所以,想法是用单个 slot 准备 LimitingExecutor。然后启动阻塞动作,它会在解除阻塞时请求取消。然后“入队”测试操作,执行时应该会失败。使用该设置,测试将调用 unblock,然后断言 test action 的任务将在等待时抛出 TaskCanceledException。
[Test]
public void RequestPropagationTest()
{
using (var setupEvent = new ManualResetEvent(initialState: false))
using (var cancellation = new CancellationTokenSource())
using (var executor = new LimitingExecutor())
{
// System-state setup action:
var cancellingTask = executor.Do(() =>
{
setupEvent.WaitOne();
cancellation.Cancel();
}, CancellationToken.None);
// Main work action:
var actionTask = executor.Do(() =>
{
throw new InvalidOperationException(
"This action should be cancelled!");
}, cancellation.Token);
// Let's wait until this `Task` starts, so it will got opportunity
// to cancel itself, and expected later exception will not come
// from just starting that action by `Task.Run` with token:
while (actionTask.Status < TaskStatus.Running)
Thread.Sleep(millisecondsTimeout: 1);
// Let's unblock slot in Executor for the 'main work action'
// by finalizing the 'system-state setup action' which will
// finally request "global" cancellation:
setupEvent.Set();
Assert.DoesNotThrowAsync(
async () => await cancellingTask);
Assert.ThrowsAsync<TaskCanceledException>(
async () => await actionTask);
}
}
public class LimitingExecutor : IDisposable
{
private const int UpperLimit = 1;
private readonly Semaphore _semaphore
= new Semaphore(UpperLimit, UpperLimit);
public Task Do(Action work, CancellationToken token)
=> Task.Run(() =>
{
_semaphore.WaitOne();
try
{
token.ThrowIfCancellationRequested();
work();
}
finally
{
_semaphore.Release();
}
}, token);
public void Dispose()
=> _semaphore.Dispose();
}
此问题的可执行演示(通过 NUnit)可以在 GitHub 找到。
但是,该测试的实施有时会失败(没有预期的TaskCanceledException),在我的机器上可能有十分之一的运行。这个问题的一种“解决方案”是在取消请求之后插入Thread.Sleep。即使睡眠 3 秒,这个测试有时也会失败(在 20 次运行后发现),当它通过时,通常不需要长时间的等待(我猜)。参考请见diff。
“其他问题”,是为了确保取消来自“等待时间”而不是来自Task.Run,因为ThreadPool可能很忙(其他正在执行的测试),并且在请求取消 - 这将使这个测试“假绿色”。 “通过 hack 轻松修复”是积极等待,直到第二个任务开始 - 它的 Status 变为 TaskStatus.Running。请检查branch 下的版本,看看没有这个 hack 的测试有时会是“绿色” - 所以示例错误可以通过它。
【问题讨论】:
-
我正在寻找解决方案,在系统没有任何变化的情况下进行绿色测试。我不确定这个测试是否很好,甚至对我们的系统是否必要,因为通过的操作也应该检查令牌。我认为我们可以放弃它,但这对我来说更像是挑战。另外,我希望看到这个测试没有任何睡眠或黑客攻击。尽管如此,
LimitingExecutor的实现也可能会更好 - 无需从池中获取任务,然后将其冻结在信号量上,可能需要使用共享计数器和Task.Delay进行主动等待。 -
为什么不使用自定义 TaskScheduler 或更好的 an ActionBlock<T> with a limited DOP instead ?一个动作块already supports cancellation.
-
TPL 已经通过自定义 TaskScheduler 类和工厂支持您提出的问题。检查this example of a LimitingTaskScheduler。它可用于创建一个新的 TaskFactory,其任务使用该特定的 TaskScheduler。
-
感谢@Panagiotis Kanavos 指点我TPL 数据流库,因为以前我没有机会仔细研究这个库,它看起来很有趣。然而,我的(真正的)问题与转换数据和将多个异步进程绑定到流没有直接关系。它更像调度程序,只需要防止一次启动(然后执行)过多的操作。尽管如此,我不认为实现整个
TaskScheduler是正确的方向,或者会被我的团队接受为解决方案 - 感觉过于复杂。
标签: c# .net task task-parallel-library cancellation