Очереди задач в C# (.NET): System.Threading.Channels и TPL Dataflow

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Очереди задач для C# (.NET): System.Threading.Channels и TPL Dataflow

При разработке многопоточных приложений на C# (.NET) часто возникает необходимость организовать асинхронную передачу данных между производителями (producers) и потребителями (consumers). Классический паттерн Producer-Consumer решается с помощью очередей задач. В экосистеме .NET есть два мощных инструмента для этого: System.Threading.Channels (появился в .NET Core 3.0) и библиотека TPL Dataflow (часть .NET с 4.5).

В этой статье мы разберём оба подхода, их особенности и приведём практические примеры кода.

1. System.Threading.Channels — лёгкая и быстрая очередь

Пространство имён System.Threading.Channels предоставляет потокобезопасные, асинхронные очереди, оптимизированные для сценариев с высокой пропускной способностью. Основные типы:

  • Channel<T> — абстрактный класс канала.
  • Channel.CreateUnbounded<T>() — неограниченная очередь (риск переполнения памяти).
  • Channel.CreateBounded<T>(int capacity) — ограниченная очередь с контролем давления.
  • ChannelWriter<T> — интерфейс для записи.
  • ChannelReader<T> — интерфейс для чтения.

Пример: Producer-Consumer с Channel

Создадим простой пример, где один производитель генерирует числа, а один потребитель их обрабатывает.

using System.Threading.Channels;
using System.Threading.Tasks;

var channel = Channel.CreateUnbounded<int>();

// Producer
var producer = Task.Run(async () =>
{
    for (int i = 0; i < 10; i++)
    {
        await channel.Writer.WriteAsync(i);
        Console.WriteLine($"Произведено: {i}");
        await Task.Delay(100);
    }
    channel.Writer.Complete(); // Сигнал об окончании
});

// Consumer
var consumer = Task.Run(async () =>
{
    await foreach (var item in channel.Reader.ReadAllAsync())
    {
        Console.WriteLine($"Обработано: {item * 2}");
        await Task.Delay(200);
    }
});

await Task.WhenAll(producer, consumer);
Console.WriteLine("Готово!");

Преимущества Channels:

  • Минимальные накладные расходы (без выделения дополнительных объектов).
  • Встроенная поддержка async/await и IAsyncEnumerable.
  • Идеально для высоконагруженных систем (например, обработка сетевых пакетов).

2. TPL Dataflow — мощный конвейер обработки

Библиотека System.Threading.Tasks.Dataflow предоставляет более сложные блоки для построения конвейеров (pipeline) и сетей обработки данных. Основные блоки:

  • BufferBlock<T> — простая очередь.
  • ActionBlock<T> — блок, выполняющий действие над каждым элементом.
  • TransformBlock<TInput, TOutput> — преобразует каждый элемент.
  • BatchBlock<T> — группирует элементы в батчи.

Пример: Конвейер с ActionBlock

Реализуем ту же задачу: производитель отправляет числа, потребитель обрабатывает.

using System.Threading.Tasks.Dataflow;

var actionBlock = new ActionBlock<int>(async item =>
{
    await Task.Delay(200);
    Console.WriteLine($"Обработано: {item * 2}");
}, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 2 });

// Producer
for (int i = 0; i < 10; i++)
{
    await actionBlock.SendAsync(i);
    Console.WriteLine($"Произведено: {i}");
    await Task.Delay(100);
}

actionBlock.Complete();
await actionBlock.Completion;
Console.WriteLine("Готово!");

Преимущества TPL Dataflow:

  • Гибкая настройка параллелизма (MaxDegreeOfParallelism).
  • Поддержка отмены через CancellationToken.
  • Возможность связывать блоки в сложные графы (например, TransformBlock → ActionBlock).
  • Встроенное управление давлением (bounded capacity).

3. Сравнение Channels и TPL Dataflow

КритерийSystem.Threading.ChannelsTPL Dataflow

Рекомендаций