День 1406. #ЗаметкиНаПолях #AsyncTips
Планирование работы в пуле потоков
Любой блок кода должен выполняться в каком-то потоке. Планировщик (scheduler) решает, где должен выполняться тот или иной код. Обычно планировщик по умолчанию работает как надо. Например, оператор await в асинхронном коде автоматически возобновит выполнение метода в том же контексте, если только вы не переопределите это поведение.
Задача: имеется фрагмент кода, который должен выполняться в потоке из пула потоков.
Решение
В большинстве случаев следует использовать Task.Run. Следующий пример блокирует поток из пула на 2 секунды:
Task task = Task.Run(() =>
{
Thread.Sleep(2000);
});
Task.Run также поддерживает возвращаемые значения и асинхронные лямбда-выражения. Задача, возвращаемая Task.Run в следующем коде, завершится через 2 секунды с результатом 13:
Task<int> task = Task.Run(async () =>
{
await Task.Delay(2000);
return 13;
});
Task.Run возвращает объект Task (или Task<T>), который может естественным образом потребляться асинхронным или реактивным кодом.
Task.Run идеально подходит для UI-приложений с продолжительной работой, которая не должна выполняться в UI-потоке. Тем не менее не используйте Task.Run в ASP.NET, если только вы не уверены в том, что делаете. В ASP.NET код обработки запросов уже выполняется в потоке из пула, так что перенесение его в другой поток из пула обычно нерационально.
Task.Run является фактической заменой для BackgroundWorker, Delegate.BeginInvoke и ThreadPool.QueueUserWorkItem. Ни один из этих старых API не следует использовать в новом коде; код с Task.Run намного проще писать и сопровождать.
Task.Run справляется с большинством задач, для которых используется Thread, так что в большинстве случаев Thread может заменяться на Task.Run (за редким исключением).
Параллельный код и код потоков данных выполняется в пуле потоков по умолчанию, поэтому обычно Task.Run не нужно использовать с кодом, выполняемым через Parallel, библиотекой TPL Dataflow или Parallel LINQ.
Если вы применяете динамический параллелизм, используйте Task.Factory.StartNew вместо Task.Run. Это необходимо из-за того, что у объекта Task, возвращаемого Task.Run, параметры по умолчанию настроены для асинхронного использования (т.е. для потребления в асинхронном или реактивном коде). Кроме того, он не поддерживает такие расширенные возможности, как задачи «родитель/потомок», типичные для динамического параллельного кода.
Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.