自我取消任务会抑制其他实例

问题描述 投票:0回答:1

我正在尝试创建一个 Task 的子类,它会自行取消并等待它被释放。 在单元测试时,我遇到了奇怪的失败测试用例。最终归结为重复。我什至可以多次运行相同的测试,并且仅在第一次运行中获得通过。

这是我的 AutoCancelTask 类的代码(抱歉代码相当长......我试图制作一个可以复制粘贴的最小功能示例......):

using System;
using System.Threading.Tasks;
using System.Threading;

namespace Tests
{
    /// <summary>
    /// A Task that is cancelled and waited for, when disposed.
    /// </summary>
    public class AutoCancelTask : Task
    {
        private readonly CancellationTokenSource _cts;

        private AutoCancelTask(Action<CancellationToken> action, CancellationTokenSource cts, TaskCreationOptions options)
            : base(() => action(cts.Token), cts.Token, options)
        {
            _cts = cts;
        }

        /// <summary>
        /// Wrap a cancellable Action in a Task that is automatically cancelled and waited for when disposed.
        /// </summary>
        /// <param name="action">The Action to wrap.</param>
        /// <param name="options">The TaskCreationOptions to use.</param>
        /// <returns>The started task.</returns>
        public static Task Run(Action<CancellationToken> action, TaskCreationOptions options = TaskCreationOptions.None)
        {
            Task t = new AutoCancelTask(action, new CancellationTokenSource(), options);
            t.Start();
            return t;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                try
                {
                    _cts.Cancel();
                }
                catch (ObjectDisposedException)
                {
                    // nothing to do in this case
                }
                try
                {
                    Wait();
                }
                catch (ObjectDisposedException) { }
                catch (AggregateException ex)
                {
                    if (1 < ex.InnerExceptions.Count || false == (ex.InnerExceptions[0] is TaskCanceledException))
                    {
                        // rethrow if the aggregate does not contain a single TaskCancelledException
                        throw ex;
                    }
                }
                _cts.Dispose();
            }

            base.Dispose(disposing);
        }
    }
}

这是我的测试课

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

namespace Tests
{
    public class TestDataWrapper
    {
        internal bool? IsCancelled { get; private set; } = null;
        internal void Action(CancellationToken cancel)
        {
            IsCancelled = Task.Delay(100, cancel).ContinueWith(t => t.IsCanceled).Result;
        }
    }

    [TestClass()]
    public class Tests
    {
        public static IEnumerable<object[]> Datas { get; } =
            Enumerable.Range(0, 5).Select(_ => new object[] { new TestDataWrapper() });

        [DataTestMethod]
        [DynamicData(nameof(Datas))]
        public void AutoCancelTask_Cancels(TestDataWrapper data)
        {
            using (AutoCancelTask.Run(data.Action)) { }
            Assert.AreEqual(true, data.IsCancelled);
        }
    }
}

显然,我期望测试用例能够通过。但是,只有第一个执行通过,而其他执行失败并显示消息

Assert.AreEqual failed. Expected:<True>. Actual:<(null)>.

有趣的是,执行时间也有所不同: 运行 1:~30 毫秒 运行 2:~300 毫秒 运行 4-5: <1ms

由于结果为空且不为假,我怀疑问题出在任务中的某个位置,甚至在操作开始之前就被取消了。然而,这让我感到困惑,因为我没有使用任何静态的或在测试运行之间共享的东西。

为了确保我正确理解

ContinueWith
表达式,我尝试将
Action
方法替换为以下内容:

internal void Action(CancellationToken cancel)
{
    Task t = Task.Delay(100, cancel);
    try
    {
        t.Wait();
    }
    catch
    {
        // ignore
    }
    IsCancelled = t.IsCanceled;
    t.Dispose();
}

这会产生相同的结果。

然后我尝试用调试输出向我的班级发送垃圾邮件,突然间我所有的测试运行都通过了。当我在此处的 Dispose 方法中添加延迟时,它也有效:

if (disposing)
{
    try
    {
        Delay(1).Wait(); // <-- this fixes it
        _cts.Cancel();
    }
    catch (ObjectDisposedException)
    {
        // nothing to do in this case
    }
}

这更加坚定了我的怀疑,即后续任务不能在另一个任务取消后太快开始......

我宁愿避免那里的延迟,因为这对我来说似乎不正确。你能为我解释一下这里发生了什么吗?

c# task task-parallel-library mstest .net-4.8
1个回答
0
投票

您的任务很可能在有机会运行之前就被取消了。开始任务 会将其放入队列中以供稍后执行。在此测试中,您将在创建任务后立即取消任务。

因此,当任务被拿起执行时,会检查其取消标记,如果取消,则直接将其设置为已取消状态。所以

data.Action
永远不会运行,并且你的标志永远不会设置。添加延迟将为框架提供足够的时间来开始执行任务。

请注意,这种方法似乎很容易出现死锁。如果您在 UI 线程上使用此 AutoCancelTask,并且该任务需要将任何内容分派到同一 UI 线程,则会出现死锁。 该任务无法完成,因为它等待 UI 线程,并且 UI 线程无法继续,因为它正在等待该任务。

© www.soinside.com 2019 - 2024. All rights reserved.