繁体   English   中英

取消请求向所有任务的传播时间(TPL)

[英]Propagation time of cancellation request to all tasks (TPL)

使用TPL,我们有CancellationTokenSource ,它提供令牌,可用于以协作方式取消当前任务(或其开始)。

题:

将取消请求传播到所有已挂起的正在运行的任务需要多长时间? 有什么地方的代码可以用来检查以下内容:从现在开始,每一个感兴趣的Task都会发现已请求取消吗?


为什么需要它?

我想进行稳定的单元测试,以表明取消在我们的代码中有效。

问题详细信息:

我们有产生任务的“执行程序”,这些任务包装了一些长时间运行的动作。 执行程序的主要工作是限制启动了多少个并发动作。 所有这些任务都可以单独取消,并且这些操作也将在内部遵守CancellationToken

我想提供单元测试,该测试表明当任务等待插槽开始给定操作时发生取消操作时 ,该任务将自行取消(最终),并且不会开始执行给定操作

因此,想法是用单个插槽准备LimitingExecutor 然后开始阻止操作 ,该操作将在取消阻止时请求取消。 然后“排队” 测试操作 ,该操作应在执行时失败。 使用该设置,测试将调用unblock ,然后断言测试操作的任务将在等待时抛出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可能很忙(其他正在执行测试),并且它在取消请求后将第二个任务的启动推迟-这将导致此测试为“伪绿色”。 “通过黑客轻松解决”是主动等待直到第二个任务开始-其Status变为TaskStatus.Running 请检查此分支下的版本,看看没有此hack的测试有时会是“绿色”-因此示例错误可能会通过。

您的测试方法假定cancellingTask始终在actionTask之前actionTask LimitingExecutor的插槽(输入信号量)。 不幸的是,这个假设是错误的, LimitingExecutor不能保证这一点,这只是运气,这两个任务中的哪一个占用了插槽(实际上在我的计算机上,它仅发生在5%的运行次数中)。

要解决此问题,您需要另一个ManualResetEvent ,它将允许主线程等待,直到cancellingTask实际占用插槽为止:

using (var slotTaken = new ManualResetEvent(initialState: false))
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(() =>
    {
        // This is called from inside the semaphore, so it's
        // certain that this task occupies the only available slot.
        slotTaken.Set();

        setupEvent.WaitOne();
        cancellation.Cancel();
    }, CancellationToken.None);

    // Wait until cancellingTask takes the slot
    slotTaken.WaitOne();

    // Now it's guaranteed that cancellingTask takes the slot, not the actionTask

    // ...
}


.NET Framework不提供用于检测任务向Running状态转换的API,因此,如果您不喜欢在循环中轮询State属性+ Thread.Sleep() ,则需要将LimitingExecutor.Do()修改为提供此信息,可能使用另一个ManualResetEvent ,例如:

public Task Do(Action work, CancellationToken token, ManualResetEvent taskRunEvent = null)
    => Task.Run(() =>
    {
        // Optional notification to the caller that task is now running
        taskRunEvent?.Set();

        // ...
    }, token);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM