Comments 10
Если вам всё-таки нужно запустить долгую задачу
Под "долгой" задачей подразумевается задача выполняющаяся более 20 мс? Нужно ли учитывать время ожидания в асинхронных операциях, или же одна действительно асинхронная операция для ThreadPool представляет две операции до и после асинхронного ожидания?
Спасибо за вопросы!
Под "долгой" задачей подразумевается задача выполняющаяся более 20 мс?
Вообще на тредпуле можно запускать задачи и дольше, просто нужно помнить, что чем задача дольше, тем хуже это может сказаться на перформансе.
Конкретно эта статья не дает ответ на вопрос, какого размера должна быть задача, поскольку рассматривался только один кусочек тредпула.
В следующей статье, возможно, попробуем погрузить тредпул на ранзых вводных и посмотреть на его состояние - там и попробуем определить некотрые границы.
Нужно ли учитывать время ожидания в асинхронных операциях, или же одна действительно асинхронная операция для ThreadPool представляет две операции до и после асинхронного ожидания?
Строго говоря, зависит.
Если ваш поток можно отпустить между операциями await (например, у вас IO задача), то это будут "две" операции.
Если поток отпустить нельзя, то это будет одна большая операция.
Hidden text
Можно проверить на небольшом примерчике:
static Task Spin()
{
for (var i = 0; i < 1000; i++)
{
}
return Task.CompletedTask;
}
static async Task SomeLongOperationCantChange(int opId)
{
Console.WriteLine($"{opId}-{Environment.CurrentManagedThreadId}");
await Spin();
Console.WriteLine($"{opId}-{Environment.CurrentManagedThreadId}");
await Spin();
Console.WriteLine($"{opId}-{Environment.CurrentManagedThreadId}");
}
public static void Main()
{
SomeLongOperationCantChange(1);
SomeLongOperationCantChange(2);
}
Здесь поток отпустить нельзя, поэтому для всех будет выведено последовательно 3 раза 1-1, потом 3 раза 2-2, несмотря на await'ы (похожее поведение будет и при Task.Run)
Для проверки обратной ситуации можно использовать Task.Delay (он не совсем IO, но работает достаточно хитро для того, чтобы уметь отпустить поток)
static async Task SomeLongOperation(int opId)
{
Console.WriteLine($"{opId}-{Environment.CurrentManagedThreadId}");
await Task.Delay(10);
Console.WriteLine($"{opId}-{Environment.CurrentManagedThreadId}");
await Task.Delay(10);
Console.WriteLine($"{opId}-{Environment.CurrentManagedThreadId}");
}
public static void Main()
{
Task.Run(()=>SomeLongOperation(1));
Task.Run(()=>SomeLongOperation(2));
Thread.Sleep(100000);
}
Скорее всего вы увидите, как операции начинают жонглировать тредами.
Благодаря этому поведению в том же aspnet'e тредпул не начинает сходить с ума при долгих обращениях в базу, например
чем задача дольше, тем хуже это может сказаться на перформансе
А можно немного раскрыть мысль?
Конечно, даже нужно, но, чтобы быть более объективным, я это сделаю в отдельной статье - можете подписаться, чтобы не пропустить ;)
Видимо, имелась в виду способность ThreadPool-а планировать свою загрузку, работа этого самого HillClimbing-а. У Дэвида Фаулера есть гайдлайн по async/await, где упоминается, что запускать таску с опцией TaskCreationOptions.LongRunning как бы бессмысленно, посколько при первом неблокирующем ожидании IO-операции этот самый поток, на создание которого было потрачено относительно много усилий, будет отпущен.
В итоге в любом случае приходим к тому, что любой современный асинхронный код - это замысловатое переплетение коротких (и длинных) CPU-bound с IO-bound операциями. Что как бы отлично ложится на работу HillClimbing-а.
Просто утверждение было слишком общее и (как минимум поэтому) слишком некорректное.
С точки зрения перфоманса, несколько долгих задач будут гораздо эффективнее множества коротких - расходы (как процессора, так и памяти) на постановку задачи в очередь, создание стейт-машины, таска, регистрации комплишна, переключение контекста (опционально), GC, чтобы собрать весь этот мусор и ещё множества необходимых операций часто гораздо выше, чем время и ресурсы необходимые на выполнение самой полезной нагрузки.
Так что ещё раз: с точки зрения чистого перфоманса, несколько долгих задач будут гораздо выгоднее множества коротких.
А дальше, конечно, начинаются нюансы - что важнее, пропускная способность, общее время выполнения или "отзывчивость", сколько (много ли) задач нужно создать "авансом" или есть возможность организовать ограниченный буфер создаваемых задач (backpressure) и т.д., и т.п.
Но, повторюсь: с точки зрения чистой производительности главное правило такое: всё, что можно собирать в пакет и запускать как единый таск - должно собираться в пакет, а уж потом нужно смотреть, что из важных характеристик пострадало и корректировать.
Спасибо за более раскрытый комментарий!
Мне кажется, что мы говорим об одном и том же, но немного по-разному
Статья была обзорной, поэтому в ней (и после нее) я не мог не использовать такие общие утверждения. Получился generic ответ для generic ситуаций, поэтому получилось не очень объективно, о чем я выше и сказал
Я говорил именно про долгие CPU-bound таски, которые тредпул плохо переваривает. Если там переплетение из IO и CPU тасок, то это нормально и хорошо укладывается в сценарии тредпула, поэтому такие "долгие" таски запускать на тредпуле нужно
Это generic правило для большей части ситуаций, с которыми можно столкнуться при разработке современного приложения на дотнете (и именно таким инструментом и является тредпул дотнета, generic инструмент для решения большей части ситуаций)
Но, конечно, не все ситуации одинаковые и где-то нужно конкретно смотреть, так что "запустить все долгие cpu-bound таски" тоже вполне себе может быть решением в конкретной ситуации
Спасибо) Всё-таки под асинхронным ожиданием я имел в виду общение со сторонними ресурсами с использованием "асинхронного драйвера", то есть второй пример.
ThreadPool – инъекция потоков