Task.Factory.StartNew и Длительные Асинхронные Задачи
Допустим, вы хотите реализовать шаблон «производитель-потребитель» на основе каналов для асинхронной обработки элементов:
public class Processor
{
private Channel<string> _channel =
Channel.CreateUnbounded<string>();
private Task _task;
public Processor()
{
_task = Task.Factory.StartNew(async () =>
{
await foreach (var log in
_channel.Reader.ReadAllAsync())
{
// Обработка
Console.WriteLine(log);
}
}, TaskCreationOptions.LongRunning);
}
public void ProcessLog(string log)
{
_channel.Writer.TryWrite(log);
}
}
И поскольку вы знаете, что задача обработки должна выполняться в течение всего процесса, вы используете флаг TaskCreationOptions.LongRunning.
Как думаете, есть здесь проблема? На самом деле, да.
Флаг LongRunning сообщает TPL о необходимости иметь выделенный поток для данного обратного вызова вместо получения потока из пула потоков. Но даже если ваша задача семантически долго выполняется, выделенный поток не будет выполняться долго.
Давайте упростим код и добавим немного трассировки:
static void WriteLine(string msg)
{
Console.WriteLine(
$"""
[{Environment.CurrentManagedThreadId}]
[IsThreadPoolThread: {Thread.CurrentThread.IsThreadPoolThread}]
{msg}.
"""
);
}
var task = Task.Factory.StartNew(async () =>
{
WriteLine("Task started");
await Task.Delay(1000);
WriteLine("Task completed");
}, TaskCreationOptions.LongRunning);
Thread.Sleep(100);
Console.WriteLine(
$"Completed: " + task.IsCompleted);
Console.ReadLine();
Вывод:
[4]
[IsThreadPoolThread: False]
Task started.
Completed: True
[6]
[IsThreadPoolThread: True]
Task completed.
Вот что происходит во время выполнения:
-
Task started печатается из выделенного потока.-
Task completed печатается из потока пула потоков.Задача, по-видимому, завершена до выполнения обратного вызова. Это происходит, потому что Task.Factory.StartNew не является асинхронно-дружественным. Фактический тип переменной задачи — Task<Task>, а родительская задача завершается, когда новый поток начинает выполнять обратный вызов, а не когда завершается сам обратный вызов.
Флаг LongRunning учитывается для запуска первого блока асинхронного метода перед первым await. Await приостанавливает выполнение асинхронного метода, а остальная часть метода планируется в потоке пула потоков планировщиком задач по умолчанию.
Теоретически возможно, что обратный вызов, который вы предоставляете StartNew, имеет длительную часть перед первым await, и вы действительно хотите запустить его в выделенном потоке. Если это так, флаг LongRunning допустим, но это очень редкое явление.
Рекомендации для Task.Factory.StartNew и асинхронных делегатов:
1. Избегайте использования Task.Factory.StartNew с асинхронными делегатами. Если необходимо, используйте метод расширения Unwarp, чтобы получить фактическую базовую задачу.
2. Не используйте флаг LongRunning с Task.Factory.StartNew для асинхронных обратных вызовов. Флаг полезен для синхронных методов, которые блокируют поток, но не для асинхронных методов, где продолжение будет запланировано в пуле потоков.
Источник: https://sergeyteplyakov.github.io/Blog/csharp/2024/10/01/Task.Factory.StartNew-and-long-running-async-tasks.html