感谢您提供的 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 的其余部分控制器操作以完成响应。