简体   繁体   English

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

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

I'm having an issue with HTTP request cancellations in a blazor server app.我在 blazor 服务器应用程序中遇到 HTTP 请求取消问题。 I have a web app that uses Google Maps with custom tile overlays.我有一个 web 应用程序,它使用带有自定义瓷砖覆盖的谷歌地图。 The map will request 256x256 tiles from my server as the user zooms around.当用户缩放时,map 将向我的服务器请求 256x256 瓦片。 As the user zooms around, Google Maps, appropriately, cancels any pending HTTP requests that are no longer needed.当用户放大时,Google 地图会适当地取消任何不再需要的待处理 HTTP 请求。 If, however, the user moves around quickly, a lot of cancellations happen.但是,如果用户快速移动,则会发生很多取消。 Something in the server is getting delayed because of all the cancellations.由于所有取消,服务器中的某些内容被延迟。 To attempt to debug this, I made a simple console app that uses Task.Delay taking a cancellation token.为了尝试调试它,我制作了一个简单的控制台应用程序,它使用 Task.Delay 获取取消令牌。 With relatively few tasks going (2x # of logical cores)), things work as expected.由于执行的任务相对较少(逻辑核心数为 2 倍)),事情按预期工作。 As the number of tasks increases, the delay becomes extreme.随着任务数量的增加,延迟变得极端。 I've made a GitHub repo demonstrating all of this at: https://github.com/TNT0305/TestWait我制作了一个 GitHub 存储库,在以下位置演示了所有这些: https://github.com/TNT0305/TestWait

Near the top of main, there are some configuration parameters:在 main 顶部附近,有一些配置参数:

  • induceDelay - set to true to increase the number of tasks to manifest the issue (default to false)诱导延迟 - 设置为 true 以增加显示问题的任务数量(默认为 false)
  • taskDelayMs - number of milliseconds to Task.Delay in each call (defaults to 20000ms) taskDelayMs - 每次调用中 Task.Delay 的毫秒数(默认为 20000 毫秒)

On my machine, with induceDelay set to true, I get 512 calls to the task.在我的机器上,将inducedDelay 设置为true,我收到512 次任务调用。 After 100ms, I call Cancel() on the cancellation token source. 100 毫秒后,我在取消令牌源上调用 Cancel()。 It takes 18.4 seconds for the call to Cancel() to return.对 Cancel() 的调用需要 18.4 秒才能返回。

I think I must be missing something.我想我一定错过了什么。 Any ideas?有任何想法吗?

Clarification澄清

The issues are not, necessarily, how long it takes for.Cancel() to return.问题不一定是 for.Cancel() 返回需要多长时间。 It has to do with how long it takes for the cancel to be detected in the async task after the.Cancel() call was initiated.它与启动 .Cancel() 调用后在异步任务中检测到取消需要多长时间有关。

Here's the Program.cs from the GitHub repo:这是 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;
            }
        }
    }
}

Bah.呸。 Slow exception reporting in Visual Studio when run in debug mode.在调试模式下运行时,Visual Studio 中的异常报告缓慢。 When I run it in release mode, it runs as expected.当我在发布模式下运行它时,它会按预期运行。 My original problem involves more components than my example uses and the downstream code had issues that I already corrected.我最初的问题涉及的组件比我的示例使用的要多,并且下游代码存在我已经纠正的问题。

Takeaway: If you are having performance issues, try it outside the debugger (Ctrl+F5 instead of F5, in VS).要点:如果您遇到性能问题,请在调试器之外尝试(在 VS 中使用 Ctrl+F5 而不是 F5)。 >.< >.<

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

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