Спасибо за предоставленные комментарии. Предложение проверить TaskCompletionSource было фундаментальным. Поэтому моя цель состояла в том, чтобы потенциально иметь сотни или тысячи запросов API в ASP.NET Core и иметь возможность обслуживать только часть из них в заданный период времени (из-за внутренних ограничений), выбирая, какие из них следует обслуживать в первую очередь, и удерживать другие, пока бэкэнды не освободятся или не отклонят их позже. Делать все это с потоками пула потоков - это плохо: блокировать / удерживать и принимать тысячи за короткое время (рост пула потоков).
Целью разработки был запрос заданий на перемещение их обработки из потоков ASP.NET в потоки без пула. Я планирую предварительно создать их в разумных количествах, чтобы избежать накладных расходов на их постоянное создание. Эти потоки реализуют общий механизм обработки запросов и могут быть повторно использованы для последующих запросов. Блокировка этих потоков для управления установлением приоритетов запросов не является проблемой (с использованием синхронизации), большинство из них не будут использовать ЦП постоянно, а объем занимаемой памяти будет управляемым. Наиболее важным является то, что потоки пула потоков будут использоваться только в самом начале запроса и сразу же освобождаться, чтобы использоваться только после того, как запрос будет завершен и вернет ответ удаленным клиентам.
Решение состоит в том, чтобы создать объект 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, который используется и не использует no объединенные потоки за всю продолжительность обработки . Когда Run.Task выполняет TrySetResult, запускает объединенный поток в течение краткого момента, чтобы вызвать остальную часть кода действия контроллера, причина в том, что количество рабочих потоков на мгновение равно 2, затем новый объединенный поток запускает остальную часть ASP.NET действие контроллера для завершения с ответом.