Как выполнить ядро ​​ASP.NET вне исходного объединенного потока в поток без объединения? - PullRequest
0 голосов
/ 03 июля 2018

Рассмотрим нормальный сценарий, когда приложение ASP.NET Core Web API выполняет действие контроллера службы, но вместо того, чтобы выполнять всю работу в одном потоке (потоке пула потоков) до создания ответа, я хотел бы использовать не -пулевые потоки (в идеале предварительно созданные) для выполнения основной работы, либо путем планирования одного из этих потоков из исходного потока пулов в результате действия и освобождения пулированного потока для обслуживания других входящих запросов, либо передачи задания предварительно созданным не- объединенная нить.

Среди прочих причин, основная причина наличия этих не пулованных и долго работающих потоков заключается в том, что некоторым запросам может быть назначен приоритет, а их потоки приостановлены (синхронизированы), поэтому он не будет блокировать новые входящие запросы к API из-за истощение пула потоков, но более ранние запросы на удержание (потоки без пула) могут быть разбужены и отклонены, и что-то вроде обратного вызова в пул потоков для возврата веб-ответа клиентам.

Таким образом, идеальным решением было бы использование механизма синхронизации (например, .NET RegisterWaitForSingleObject ), где объединенный в поток поток мог бы подключиться к waitHandle, но был бы освобожден для работы других пулов потоков, и новый не поток будет создан или использован для продолжения выполнения. В идеале из списка предварительно созданных и свободных потоков без пула.

Кажется, что async-await работает только с Задачами и потоками из пула потоков .NET, но не с другими потоками. Кроме того, большинство методов создания потоков без пула не позволяют освободить поток из пула и вернуться в пул.

Есть идеи? Я использую .NET Core и последние версии инструментов и платформ.

1 Ответ

0 голосов
/ 04 июля 2018

Спасибо за предоставленные комментарии. Предложение проверить 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 действие контроллера для завершения с ответом.

...