如何在初始池线程之外对非池线程执行 ASP.NET Core 执行?

Posted

技术标签:

【中文标题】如何在初始池线程之外对非池线程执行 ASP.NET Core 执行?【英文标题】:How to perform ASP.NET Core execution outside of the initial pooled thread to a non-pooled thread? 【发布时间】:2018-12-11 23:51:34 【问题描述】:

考虑正常的场景,ASP.NET Core Web API 应用程序执行服务控制器动作,而不是在创建响应之前执行同一线程(线程池线程)下的所有工作,我想使用非-池线程(理想情况下是预先创建的)来执行主要工作,或者通过从初始操作池线程中调度这些线程之一并释放池线程以服务其他传入请求,或者将作业传递给预先创建的非-线程池。

除其他原因外,拥有这些非池化且长时间运行的线程的主要原因是某些请求可能被优先处理,并且它们的线程被搁置(同步),因此它不会阻止对 API 的新传入请求,因为线程池饥饿,但保留的较旧请求(非池线程)可能会被唤醒并被拒绝,并且可能会回调线程池以将 Web 响应返回给客户端。

总而言之,理想的解决方案是使用一种同步机制(如 .NET RegisterWaitForSingleObject),池中的线程会挂接到 waitHandle,但可以腾出空间用于其他线程池工作,以及一个新的非池线程将被创建或用于继续执行。理想情况下来自预先创建和空闲的非池线程列表。

似乎 async-await 仅适用于 .NET 线程池中的任务和线程,不适用于其他线程。此外,大多数创建非池线程的技术都不允许池线程空闲并返回池。

有什么想法吗?我正在使用 .NET Core 和最新版本的工具和框架。

【问题讨论】:

ideally pre-created 换句话说,池化。您所描述的是 ASP.NET 已经在做的事情。每个请求都由来自预创建线程池的不同线程提供服务。没有阻塞,没有饥饿。可以使用await Task.Run(..) 执行长时间运行的任务,而不会阻塞原始线程。并不是说它会有所帮助,您仍在使用 a 线程。还不如用原来的 唯一需要“不同”线程的情况是在进行异步调用时,例如对另一个服务、数据库或执行 IO。在这种情况下,除了等待之外,线程没有什么可做的。您可以使用异步方法,例如 HttpClient.GetStringAsyncExecuteReaderAsync 等,而不是等待。在这种情况下,使用 no 线程。池化线程回到池中,可用于其他请求。当异步调用完成时,从池中拉出一个线程(不是原始线程)来处理响应并执行其余的调用 您想解决的实际问题是什么?您所描述的是 TPL 和 ASP.NET 已经如何工作。它甚至具有工作窃取功能,因此空闲线程可以从繁忙线程的队列中“窃取”工作。这听起来像是the XY Problem 的情况。您遇到问题 X 并假设 Y 是解决方案。当 Y 不工作时,你要的是 Y,而不是 X 感谢您的 cmets Panagiotis。预先创建意味着它们正在运行处于空闲状态的线程,由队列提供服务。这些线程将对每个请求执行大量操作和检查并运行服务逻辑,但应该已经实例化。这意味着不会有正常的开销或调度新线程并实例化所有内部对象。完成一个请求后,一个线程可以立即在另一个排队的线程上启动。线程池线程被分配工作,但必须为每个请求重新创建整个上下文。有意义吗? 我再说一遍,这正是线程池是什么。这正是池化线程的内容。如果您担心线程池的增长速度,您可以增加其最小大小。如果您担心阻塞,请不要阻塞。如果您想使用具有不同优先级的不同请求,a) 这可能意味着您需要两个不同的服务,而不是一个,并且 b) 已经有机制来处理它。无论是具有不同 DOP 设置的不同 TaskScheduler 或 ActionBlock<T> 实例,还是使用 TaskCompletionSource 模拟等待 【参考方案1】:

感谢您提供的 cmets。检查 TaskCompletionSource 的建议很重要。所以我的目标是在 ASP.NET Core 上可能有成百上千的 API 请求,并且能够在给定的时间范围内只服务其中的一部分(由于后端限制),选择应该首先提供哪些 API 并保留其他人,直到后端免费或稍后拒绝它们。用线程池线程做这一切是不好的:阻塞/保持并且不得不在短时间内接受数千(线程池大小增加)。

设计目标是将其处理从 ASP.NET 线程转移到非池线程的请求作业。我计划以合理的数量预先创建这些,以避免一直创建它们的开销。这些线程实现了一个通用的请求处理引擎,可以重复用于后续请求。阻塞这些线程来管理请求优先级不是问题(使用同步),它们中的大多数不会一直使用 CPU 并且内存占用是可控的。最重要的是,线程池线程只会在请求开始时使用并立即释放,仅在请求完成后使用并向远程客户端返回响应。

解决方案是创建一个 TaskCompletionSource 对象并将其传递给可用的非池线程来处理请求。这可以通过根据服务类型和客户端的优先级将请求数据与任务完成源对象一起排队到正确的队列中来完成,或者如果没有可用的线程,则将其传递给新创建的线程。 ASP.NET 控制器操作将在 TaskCompletionSouce.Task 上等待,一旦主处理线程在此对象上设置结果,控制器操作的其余代码将由池线程执行并将响应返回给客户端。同时,可以终止主处理线程,也可以从队列中获取更多的请求作业。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace MyApi.Controllers

    [Route("api/[controller]")]
    public class ValuesController : Controller
    
        public static readonly object locker = new object();
        public static DateTime time;
        public static volatile TaskCompletionSource<string> tcs;

        // GET api/values
        [HttpGet]
        public async Task<string> Get()
        
            time = DateTime.Now;
            ShowThreads("Starting Get Action...");

            // Using await will free the pooled thread until a Task result is available, basically
            // returns a Task to the ASP.NET, which is a "promise" to have a result in the future.
            string result = await CreateTaskCompletionSource();

            // This code is only executed once a Task result is available: the non-pooled thread 
            // completes processing and signals (TrySetResult) the TaskCompletionSource object
            ShowThreads($"Signaled... Result: result");
            Thread.Sleep(2_000);
            ShowThreads("End Get Action!");

            return result;
        

        public static Task<string> CreateTaskCompletionSource()
        
            ShowThreads($"Start Task Completion...");

            string data = "Data";
            tcs = new TaskCompletionSource<string>();

            // Create a non-pooled thread (LongRunning), alternatively place the job data into a queue
            // or similar and not create a thread because these would already have been pre-created and
            // waiting for jobs from queues. The point is that is not mandatory to create a thread here.
            Task.Factory.StartNew(s => Workload(data), tcs, 
                CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);

            ShowThreads($"Task Completion created...");

            return tcs.Task;
        

        public static void Workload(object data)
        
            // I have put this Sleep here to give some time to show that the ASP.NET pooled
            // thread was freed and gone back to the pool when the workload starts.
            Thread.Sleep(100);

            ShowThreads($"Started Workload... Data is: (string)data");
            Thread.Sleep(10_000);
            ShowThreads($"Going to signal...");

            // Signal the TaskCompletionSource that work has finished, wich will force a pooled thread 
            // to be scheduled to execute the final part of the APS.NET controller action and finish.
            // tcs.TrySetResult("Done!");
            Task.Run((() => tcs.TrySetResult("Done!")));
            // The only reason I show the TrySetResult into a task is to free this non-pooled thread 
            // imediately, otherwise the following line would only be executed after ASP.NET have 
            // finished processing the response. This briefly activates a pooled thread just execute 
            // the TrySetResult. If there is no problem to wait for ASP.NET to complete the response, 
            // we do it synchronosly and avoi using another pooled thread.

            Thread.Sleep(1_000);

            ShowThreads("End Workload");
        

        public static void ShowThreads(string message = null)
        
            int maxWorkers, maxios, minWorkers, minIos, freeWorkers, freeIos;

            lock (locker)
            
                double elapsed = DateTime.Now.Subtract(time).TotalSeconds;

                ThreadPool.GetMaxThreads(out maxWorkers, out maxIos);
                ThreadPool.GetMinThreads(out minWorkers, out minIos);
                ThreadPool.GetAvailableThreads(out freeWorkers, out freeIos);

                Console.WriteLine($"Used WT: maxWorkers - freeWorkers, Used IoT: maxIos - freeIos - "+
                                  $"+elapsed.ToString("0.000 s") : message");
            
        
    

我已经放置了整个示例代码,因此任何人都可以轻松地创建为 ASP.NET Core API 项目并对其进行测试,而无需进行任何更改。这是结果输出:

MyApi> Now listening on: http://localhost:23145
MyApi> Application started. Press Ctrl+C to shut down.
MyApi> Used WT: 1, Used IoT: 0 - +0.012 s : Starting Get Action...
MyApi> Used WT: 1, Used IoT: 0 - +0.015 s : Start Task Completion...
MyApi> Used WT: 1, Used IoT: 0 - +0.035 s : Task Completion created...
MyApi> Used WT: 0, Used IoT: 0 - +0.135 s : Started Workload... Data is: Data
MyApi> Used WT: 0, Used IoT: 0 - +10.135 s : Going to signal...
MyApi> Used WT: 2, Used IoT: 0 - +10.136 s : Signaled... Result: Done!
MyApi> Used WT: 1, Used IoT: 0 - +11.142 s : End Workload
MyApi> Used WT: 1, Used IoT: 0 - +12.136 s : End Get Action!

正如你所看到的,池线程一直运行到等待创建 TaskCompletionSource,并且当工作负载开始处理非池线程上的请求时 有零线程池线程正在使用并且仍然不使用在整个处理期间池化线程。当 Run.Task 执行 TrySetResult 时,会在短时间内触发一个池线程以触发控制器操作代码的其余部分,原因是 Worker 线程计数暂时为 2,然后一个新的池线程运行 ASP.NET 的其余部分控制器操作以完成响应。

【讨论】:

以上是关于如何在初始池线程之外对非池线程执行 ASP.NET Core 执行?的主要内容,如果未能解决你的问题,请参考以下文章

来自创建的非线程池线程的 ASP.NET 异常会发生啥?

asp.net多线程,如何判断所有子线程都已经运行完毕

ASP.NET Core 休息服务的自动缩放指标

线程池基本使用

线程池的实现原理

并发编程之:深入解析线程池