Динамический параллелизм
Задача: Требуется реализовать параллельную обработку, когда структура и количество параллельных задач становятся известны только во время выполнения.
Решение
Класс 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.