Обложка канала

.NET Разработчик

Опытный разработчик не так давно зашёл в .Net и поставил цель получить сертификат Microsoft. Свой ежедневный прогресс он описывает на канале .Net Разработчик. Заметки об изученном материале, советы по повышению производительности и поддержке мотивации, ин

.NET Разработчик

4 года назад
Открыть в
День 1117. #ЗаметкиНаПолях #AsyncTips Динамический параллелизм Задача: Требуется реализовать параллельную обработку, когда структура и количество параллельных задач становятся известны только во время выполнения. Решение Класс Parallel и Parallel LINQ, которые рассматривались ранее, — всего лишь удобные обертки для мощного типа Task. Если требуется реализовать динамический параллелизм, проще использовать тип Task напрямую. В примере ниже для каждого узла бинарного дерева необходимо выполнить некоторую затратную обработку. Структура дерева неизвестна до стадии выполнения. Метод Traverse обрабатывает текущий узел, а затем создаёт две дочерние задачи, по одной для каждой ветви (в данном примере предполагается, что родительские узлы должны быть обработаны до перехода к дочерним узлам):
void Traverse(Node current)
{
  DoActionOnNode(current);
  if (current.Left != null)
    Task.Factory.StartNew(
      () => Traverse(current.Left),
      CancellationToken.None,
      TaskCreationOptions.AttachedToParent,
      TaskScheduler.Default);

  if (current.Right != null)
    Task.Factory.StartNew(
      () => Traverse(current.Right),
      CancellationToken.None,
      TaskCreationOptions.AttachedToParent,
      TaskScheduler.Default);
}

Метод ProcessTree начинает обработку, создавая родительскую задачу верхнего уровня и ожидая её завершения:
void ProcessTree(Node root)
{
  Task task = Task.Factory.StartNew(
    () => Traverse(root),
    CancellationToken.None,
    TaskCreationOptions.None,
    TaskScheduler.Default);

  task.Wait();
}

Флаг AttachedToParent гарантирует, что задача для каждой ветви связывается с задачей своего родительского узла. Таким образом создаются отношения «родитель/потомок» между экземплярами Task. Родительские задачи выполняют своего делегата, после чего ожидают завершения своих дочерних задач. Исключения от дочерних задач распространяются к родительской задаче. Таким образом, ProcessTree может ожидать задач для всего дерева, для чего достаточно вызвать Wait для одной задачи в корне. Другой способ решения этой задачи - сохранить каждую новую задачу в потокобезопасной коллекции, а затем ожидать завершения их всех с использованием Task.WaitAll. Если ваша ситуация не относится к категории «родитель/потомок», вы можете запланировать запуск любой задачи после другой задачи, используя продолжение (continuation). Оно представляет собой отдельную задачу, которая выполняется после завершения исходной:
Task task = Task.Factory.StartNew(
  () => Thread.Sleep(TimeSpan.FromSeconds(2)),
  CancellationToken.None,
  TaskCreationOptions.None,
  TaskScheduler.Default);

Task continuation = task.ContinueWith(
  t => Trace.WriteLine("Task is done"),
  CancellationToken.None,
  TaskContinuationOptions.None,
  TaskScheduler.Default);

Замечание: всегда лучше явно задавать планировщик TaskScheduler, используемый в StartNew и ContinueWith. Использование Task для параллельной и асинхронной обработки принципиально отличаются: 1. Параллельные задачи: - могут использовать блокирующие методы, такие как Task.Wait, Task.Result, Task.WaitAll и Task.WaitAny, - обычно используют AttachedToParent для создания отношений «родитель/потомок» между задачами, - следует создавать методами Task.Run или Task.Factory.StartNew. 2. Асинхронные задачи: - следует использовать с await, Task.WhenAll и Task.WhenAny вместо блокирующих методов, - не должны использовать AttachedToParent, но могут формировать неявные отношения «родитель/потомок», используя ожидание других задач. Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 4.