考虑正常情况,在这种情况下,ASP.NET Core Web API应用程序执行服务Controller动作,但与其在同一线程(线程池线程)下执行所有工作,直到创建响应,不如使用-池线程(最好是预先创建的)以执行主要工作,方法是从初始操作池线程中调度这些线程之一,并释放池线程以服务其他传入请求,或者将作业传递给预创建的非线程池线程。
除其他原因外,拥有这些非池化且长期运行的线程的主要原因是,某些请求可能会被优先处理,并且它们的线程会被搁置(同步),因此,由于以下原因,它不会阻止对API的新传入请求:线程池不足,但较旧的保留请求(非池线程)可能会被唤醒并被拒绝,并且需要对线程池进行某种调用以将Web响应返回给客户端。
总而言之,理想的解决方案是使用同步机制(例如.NET RegisterWaitForSingleObject ),其中池化的线程将挂接到waitHandle上,但可以释放出来用于其他线程池工作,并且非池线程将被创建或用于执行。理想情况下,是从预先创建的和空闲的非池化线程列表中进行的。
似乎async-await仅适用于.NET线程池中的任务和线程,不适用于其他线程。同样,大多数创建非池化线程的技术也不允许池化线程释放并返回池。
有什么想法吗?我正在使用.NET Core和最新版本的工具和框架。
答案 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的其余部分。控制器动作以完成响应。