在 REST API 中使用异步/等待处理 CPU 密集型任务

Art*_*Art 2 c# performance async-await asp.net-web-api asp.net-core

我在理解 REST API 中使用 async/await 的优点时遇到问题。\n我有这个 CPU 密集型任务:

\n
        [HttpGet("GetHeavyStuffAsync")]\n        public async Task<string> GetHeavyStuffAsync()\n        {\n            Guid id = Guid.NewGuid();\n            System.Diagnostics.Debug.WriteLine($"{id} has started");\n            await _expensiveOperations.DoHeavyStuffOnDifferentThread();\n            System.Diagnostics.Debug.WriteLine($"{id} has finished");\n            return "request processed";\n        }\n
Run Code Online (Sandbox Code Playgroud)\n

这是在哪里DoHeavyStuffOnDifferentThread()

\n
        public async Task DoHeavyStuffOnDifferentThread()\n        {\n            var t = Task.Run(() =>\n            {\n                var limit = 4000;\n                var array = Enumerable.Range(0, limit).ToArray();\n                stoogesort(array, 0, array.Count() - 1);\n            });\n\n            await t;\n        }\n
Run Code Online (Sandbox Code Playgroud)\n

我使用Task.Run(() => ...的是 CPU 繁重的东西在不同的威胁中执行而不阻塞主要的威胁,并使用 async/await 希望控制器线程不会被繁重的任务阻塞并且可以继续处理请求。

\n

为了测试我编写了一个程序,该程序针对 启动了 250 个请求GetHeavyStuffAsync(),之后我从 swagger 向同一 API 控制器中的不同端点发出了请求:

\n
        [HttpGet]\n        public IEnumerable<WeatherForecast> Get()\n        {\n            System.Diagnostics.Debug.WriteLine("GETTING FORECAST ...");\n            var rng = new Random();\n            return Enumerable.Range(1, 5).Select(index => new WeatherForecast\n            {\n                Date = DateTime.Now.AddDays(index),\n                TemperatureC = rng.Next(-20, 55),\n                Summary = Summaries[rng.Next(Summaries.Length)]\n            })\n            .ToArray();\n        }\n
Run Code Online (Sandbox Code Playgroud)\n

正如您所看到的,最后一个端点是您创建 API 项目时 Visual Studio 默认创建的端点,它是一个非常简单的函数,会立即返回。

\n

我期望发生的情况:对的调用GetHeavyStuffAsync将被处理,await _expensiveOperations.DoHeavyStuffOnDifferentThread();控制权将传递给执行繁重工作的线程,API 控制器将可以自由地处理其他请求,在某个时刻DoHeavyStuffOnDifferentThread将完成,控制器将继续这条指令System.Diagnostics.Debug.WriteLine($"{id} has finished");完成该方法后,就可以自由地继续处理其他请求。

\n

实际发生的情况是public IEnumerable<WeatherForecast> Get()几分钟后才返回。

\n

那么,如果我没有\xc2\xb4t 使用不同的线程或异步/等待,那么为什么行为与我的行为没有区别呢?

\n

(注:测试期间我的笔记本电脑的CPU和内存剩余在50%左右)

\n

Gab*_*uci 7

我正在使用 Task.Run(() => ... 以便 cpu 重的东西在不同的威胁中执行,而不会阻塞主要的威胁

在 ASP.NET 中这样做没有任何好处。

在桌面应用程序中,UI 只能由主线程更新。因此,将 CPU 密集型任务卸载到另一个线程是有意义的,因为它可以释放 UI 线程以继续响应用户输入。

然而,在 ASP.NET 中,没有一个“主线程”。每个新请求都会分配一个新线程,直到达到最大线程池计数,然后任何进一步的请求都必须等待。

因此,当您使用 时Task.Run,您将释放主请求的线程,但您正在使用另一个线程。因此,对线程池计数的净影响仍然相同。

在ASP.NET Core 性能最佳实践一文中,Microsoft 建议:

不要:

  • 调用 Task.Run 并立即等待它。ASP.NET Core 已经在普通线程池线程上运行应用程序代码,因此调用 Task.Run 只会导致额外不必要的线程池调度。即使计划的代码会阻塞线程,Task.Run 也不会阻止这种情况。

异步代码仅在发出 I/O 请求(网络、文件系统等)时对您有帮助,因为在等待期间实际上无事可做。但对于 CPU 密集型任务来说,没有任何好处。