繁体   English   中英

CancellationTokenSource.Cancel() 触发注册取消委托的速度非常慢

[英]CancellationTokenSource.Cancel() very slow to trigger registered cancellation delegates

我在 blazor 服务器应用程序中遇到 HTTP 请求取消问题。 我有一个 web 应用程序,它使用带有自定义瓷砖覆盖的谷歌地图。 当用户缩放时,map 将向我的服务器请求 256x256 瓦片。 当用户放大时,Google 地图会适当地取消任何不再需要的待处理 HTTP 请求。 但是,如果用户快速移动,则会发生很多取消。 由于所有取消,服务器中的某些内容被延迟。 为了尝试调试它,我制作了一个简单的控制台应用程序,它使用 Task.Delay 获取取消令牌。 由于执行的任务相对较少(逻辑核心数为 2 倍)),事情按预期工作。 随着任务数量的增加,延迟变得极端。 我制作了一个 GitHub 存储库,在以下位置演示了所有这些: https://github.com/TNT0305/TestWait

在 main 顶部附近,有一些配置参数:

  • 诱导延迟 - 设置为 true 以增加显示问题的任务数量(默认为 false)
  • taskDelayMs - 每次调用中 Task.Delay 的毫秒数(默认为 20000 毫秒)

在我的机器上,将inducedDelay 设置为true,我收到512 次任务调用。 100 毫秒后,我在取消令牌源上调用 Cancel()。 对 Cancel() 的调用需要 18.4 秒才能返回。

我想我一定错过了什么。 有任何想法吗?

澄清

问题不一定是 for.Cancel() 返回需要多长时间。 它与启动 .Cancel() 调用后在异步任务中检测到取消需要多长时间有关。

这是 GitHub 存储库中的Program.cs

using System.Text;

namespace testWait
{
    /// <summary>
    /// Class to record information on timing of tasks
    /// </summary>
    class Event
    {
        public int Id { get; set; }

        public DateTime StartTime { get; set; }
        public DateTime EndTime { get; set; }
        public bool CancelledBeforeStart { get; set; } = false;
        public DateTime? CancelTriggerTime { get; set; }
        public DateTime? CancelExceptionTime { get; set; }

        // Properties to make sense of the recorded times
        public double TotalDuration { get => (EndTime - StartTime).TotalSeconds; }
        public double TriggeredAfter {  get => CancelTriggerTime != null ? (CancelTriggerTime.Value - StartTime).TotalSeconds : -1.0; }
        public double ExceptionAfter { get => CancelExceptionTime != null ? (CancelExceptionTime.Value - StartTime).TotalSeconds : -1.0; }

        public override string ToString() => $"{Id},{StartTime},{TotalDuration},{TriggeredAfter},{ExceptionAfter},{CancelledBeforeStart}";
        public string ReportCancel(DateTime cancelTime)
        {
            var t = CancelTriggerTime != null ? (CancelTriggerTime.Value - cancelTime).TotalSeconds : -1.0;
            return $"{Id} cancel delegate called {t}s after cts.Cancel() was called";
        }
    }
    internal class Program
    {
        static async Task Main(string[] args)
        {
            //////////////////////////////////////////////////////
            // RUN CONFIGURATION
            // set induceIssue to true to observe excessive delay.  Set to false to observe expected behaviors
            bool induceIssue = true;
            int taskDelayMs = 30000;    // Task.DelayAsync for 20 seconds
            //////////////////////////////////////////////////////

            // Hold results from all calls.  Outer Main has Id=-1
            List<Event> Events = new List<Event>();

            int taskCount = Environment.ProcessorCount << 1;    // twice as many tasks as logical cores
            if (induceIssue) taskCount = Environment.ProcessorCount << 5; // 2^5 as may tasks as cores

            Console.WriteLine($"Starting {taskCount} tasks on {Environment.ProcessorCount} Logical Cores");
            DateTime start = DateTime.Now;
            DateTime end = start;
            var te = new Event
            {
                Id = -1,
                StartTime = start,
                EndTime = end
            };
            // Add the event representing the entire "Main"
            Events.Add(te);
            using var cts = new CancellationTokenSource();
            
            // record the time when we detected the token was triggered
            cts.Token.Register(() => end = DateTime.Now);

            // create an array of cancellable tasks
            var tasks = (from i in Enumerable.Range(0, taskCount) select DoSomething(i, taskDelayMs, cts.Token)).ToArray();
            // try with Task.Run to see if it makes a difference (it does now))
            //var tasks = (from i in Enumerable.Range(0, taskCount) select Task.Run(async () => await DoSomething(i, taskDelayMs, cts.Token), cts.Token)).ToArray();

            // CancelAfter is what we want, but let's call cancel, explicitly, to observe delays
            //cts.CancelAfter(200);

            // wait 100ms to trigger the cancellation so that we have a chance to enter into the Task.Delay(...) calls
            DateTime cancelStart = DateTime.MinValue;
            var triggerTask = Task.Run(async () =>
            {
                await Task.Delay(100);
                cancelStart = DateTime.Now;
                var triggerTime = (cancelStart - start).TotalSeconds;
                Console.WriteLine($"Cancelling work after {triggerTime}s");
                cts.Cancel();
                DateTime cancelEnd = DateTime.Now;
                triggerTime = (cancelEnd - start).TotalSeconds;
                var cancelDuration = (cancelEnd - cancelStart).TotalSeconds;
                // report time at which the token source finished the call to Cancel() (observe long delay)
                Console.WriteLine($"After calling cancel: {triggerTime}s (ctr..Cancel() duration: {cancelDuration})");
                te.CancelTriggerTime = DateTime.Now;
            });

            try
            {
                // use wait instead of when to pass the token into the WaitAll rather than relying on "DoSomething"
                //Task.WaitAll(tasks, cts.Token);
                Events.AddRange(await Task.WhenAll(tasks));
            }
            catch (OperationCanceledException oce)
            {
                // records the time when the exception threw the OperationCancelledException (if it is thrown)
                te.CancelExceptionTime = DateTime.Now;
                Console.WriteLine("Main Task Cancelled Exception");
            }

            te.EndTime = DateTime.Now;

            var duration = (DateTime.Now - start).TotalSeconds;
            var cancelAfter = (end - start).TotalSeconds;

            await triggerTask;
            //wait for them all to _actually_ finish
            //Events.AddRange(await Task.WhenAll(tasks));

            #region Build results String

            var sb = new StringBuilder();
            // sort the events by when the cancellation token was triggered
            foreach (var e in Events.OrderBy(e => e.TriggeredAfter).ToList())
            {
                //sb.AppendLine(e.ToString());
                sb.AppendLine(e.ReportCancel(cancelStart));
            }

            #endregion

            // Write out all of the results
            Console.Write(sb.ToString());

            Console.WriteLine($"MainTask, taskDuration: {duration}, cancelAfter: {cancelAfter}");
            Console.WriteLine("Done processing. Press any key");
            Console.ReadKey();
        }
        static async Task<Event> DoSomething(int i, int delayMs, CancellationToken token)
        {
            Event e = new();
            //lock (Events) Events.Add(e);
            try
            {
                e.Id = i;
                //lock(log) log.AppendLine($"{i} started");
                e.StartTime = DateTime.Now;
                e.EndTime = e.StartTime;

                // record the time when we detected the token was triggered
                token.Register(() => e.CancelTriggerTime = DateTime.Now);

                if (token.IsCancellationRequested)
                {
                    e.CancelledBeforeStart = true;
                    return e;
                }
                try
                {
                    await Task.Delay(delayMs, token);
                }
                catch (TaskCanceledException tce)
                {
                    e.CancelExceptionTime = DateTime.Now;
                }
                e.EndTime = DateTime.Now;
                //await Task.Delay(20);
                return e;
            }
            finally
            {
                e.EndTime = DateTime.Now;
            }
        }
    }
}

呸。 在调试模式下运行时,Visual Studio 中的异常报告缓慢。 当我在发布模式下运行它时,它会按预期运行。 我最初的问题涉及的组件比我的示例使用的要多,并且下游代码存在我已经纠正的问题。

要点:如果您遇到性能问题,请在调试器之外尝试(在 VS 中使用 Ctrl+F5 而不是 F5)。 >.<

暂无
暂无

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

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