简体   繁体   English

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

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

With TPL we have CancellationTokenSource which provides tokens, useful to cooperatively cancellation of current task (or its start). 使用TPL,我们有CancellationTokenSource ,它提供令牌,可用于以协作方式取消当前任务(或其开始)。

Question: 题:

How long it take to propagate cancellation request to all hooked running tasks? 将取消请求传播到所有已挂起的正在运行的任务需要多长时间? Is there any place, where code could look to check that: "from now" every interested Task , will find that cancellation has been requested? 有什么地方的代码可以用来检查以下内容:从现在开始,每一个感兴趣的Task都会发现已请求取消吗?


Why there is need for it? 为什么需要它?

I would like to have stable unit test, to show that cancellation works in our code. 我想进行稳定的单元测试,以表明取消在我们的代码中有效。

Problem details: 问题详细信息:

We have "Executor" which produces tasks, these task wrap some long running actions. 我们有产生任务的“执行程序”,这些任务包装了一些长时间运行的动作。 Main job of executor is to limit how many concurrent actions were started. 执行程序的主要工作是限制启动了多少个并发动作。 All of these tasks can be cancelled individually, and also these actions will respect CancellationToken internally. 所有这些任务都可以单独取消,并且这些操作也将在内部遵守CancellationToken

I would like to provide unit test, which shows that when cancellation occurred while task is waiting for slot to start given action , that task will cancel itself (eventually) and does not start execution of given action . 我想提供单元测试,该测试表明当任务等待插槽开始给定操作时发生取消操作时 ,该任务将自行取消(最终),并且不会开始执行给定操作

So, idea was to prepare LimitingExecutor with single slot . 因此,想法是用单个插槽准备LimitingExecutor Then start blocking action , which would request cancellation when unblocked. 然后开始阻止操作 ,该操作将在取消阻止时请求取消。 Then "enqueue" test action , which should fail when executed. 然后“排队” 测试操作 ,该操作应在执行时失败。 With that setup, tests would call unblock and then assert that task of test action will throw TaskCanceledException when awaited. 使用该设置,测试将调用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();
}

Executable demo (via NUnit) of this problem could be found at GitHub . 这个问题的可执行演示(通过NUnit)可以在GitHub上找到

However, implementation of that test sometimes fails (no expected TaskCanceledException ), on my machin maybe 1 in 10 runs. 但是,该测试的实现有时会失败(没有预期的TaskCanceledException ),在我的机器上可能有十分之一的运行。 Kind of "solution" to this problem is to insert Thread.Sleep right after request of cancellation. 解决该问题的一种方法是在取消请求之后立即插入Thread.Sleep Even with sleep for 3 seconds this test sometimes fails (found after 20-ish runs), and when it passes, that long waiting is usually unnecessary (I guess). 即使睡眠3秒钟,该测试有时仍会失败(运行20次后才能发现),并且通过测试后,通常无需长时间等待(我想)。 For reference, please see diff . 作为参考,请参见diff

"Other problem", was to ensure that cancellation comes from "waiting time" and not from Task.Run , because ThreadPool could be busy (other executing tests), and it cold postpone start of second task after request of cancellation - that would render this test "falsy-green". “其他问题”,是为了确保取消来自“等待时间”,而不是来自Task.Run ,因为ThreadPool可能很忙(其他正在执行测试),并且它在取消请求后将第二个任务的启动推迟-这将导致此测试为“伪绿色”。 The "easy fix by hack" was to actively wait until second task starts - its Status becomes TaskStatus.Running . “通过黑客轻松解决”是主动等待直到第二个任务开始-其Status变为TaskStatus.Running Please check version under this branch and see that test without this hack will be sometimes "green" - so exampled bug could pass through it. 请检查此分支下的版本,看看没有此hack的测试有时会是“绿色”-因此示例错误可能会通过。

Your test method assumes that cancellingTask always takes the slot (enters the semaphore) in LimitingExecutor before the actionTask . 您的测试方法假定cancellingTask始终在actionTask之前actionTask LimitingExecutor的插槽(输入信号量)。 Unfortunatelly, this assumption is wrong, LimitingExecutor does not guarantee this and it's just a matter of luck, which of the two task takes the slot (actually on my computer it only happens in something like 5% of runs). 不幸的是,这个假设是错误的, LimitingExecutor不能保证这一点,这只是运气,这两个任务中的哪一个占用了插槽(实际上在我的计算机上,它仅发生在5%的运行次数中)。

To resolve this problem, you need another ManualResetEvent , that will allow main thread to wait until cancellingTask actually occupies the slot: 要解决此问题,您需要另一个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 doesn't provide API to detect task transition to the Running state, so if you don't like polling the State property + Thread.Sleep() in a loop, you'll need to modify LimitingExecutor.Do() to provide this information, probably using another ManualResetEvent , eg: .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