如何在从初始池线程到非池线程的外部执行ASP.NET Core?

时间:2018-07-03 15:37:43

标签: asp.net .net multithreading async-await threadpool

考虑正常情况,在这种情况下,ASP.NET Core Web API应用程序执行服务Controller动作,但与其在同一线程(线程池线程)下执行所有工作,直到创建响应,不如使用-池线程(最好是预先创建的)以执行主要工作,方法是从初始操作池线程中调度这些线程之一,并释放池线程以服务其他传入请求,或者将作业传递给预创建的非线程池线程。

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

总而言之,理想的解决方案是使用同步机制(例如.NET RegisterWaitForSingleObject ),其中池化的线程将挂接到waitHandle上,但可以释放出来用于其他线程池工作,并且非池线程将被创建或用于执行。理想情况下,是从预先创建的和空闲的非池化线程列表中进行的。

似乎async-await仅适用于.NET线程池中的任务和线程,不适用于其他线程。同样,大多数创建非池化线程的技术也不允许池化线程释放并返回池。

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

1 个答案:

答案 0 :(得分:0)

感谢您提供的评论。检查 TaskCompletionSource 的建议是基本的。因此,我的目标是在ASP.NET Core上潜在地拥有数百或数千个API请求,并且由于给定的时间限制,只能在给定的时间范围内满足其中的一部分,从而选择应首先满足并保留哪些请求。其他人,直到后端免费或稍后拒绝它们为止。用线程池线程来做所有这些事情是不好的:阻塞/保持并且必须在短时间内接受数千个线程(线程池大小不断增加)。

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

解决方案是创建一个TaskCompletionSource对象,并将其传递给可用的非池线程以处理请求。这可以通过根据服务的类型和客户端的优先级将请求数据与TaskCompletetionSource对象一起排队在正确的队列上,或者在没有可用的情况下将其传递给新创建的线程来完成。 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的等待状态,并且等到工作负载开始在非池化线程上处理请求时,正在使用ZERO ThreadPool线程,并且不使用在整个处理过程中池化线程。当Run.Task执行TrySetResult时,会短暂触发一个池化线程以触发其余的控制器操作代码,原因是Worker线程计数暂时为2,然后一个新的池化线程运行了ASP.NET的其余部分。控制器动作以完成响应。