В Лекции 2 были рассмотрены параллельные реализации циклов For и ForEach. Еще один способ распараллеливания, поддерживаемый классом - это метод .
Статический метод позволяет распараллелить исполнение
Подобные ситуации часто возникают в рекурсивных алгоритмах и алгоритмах типа "разделяй и властвуй". Рассмотрим, например, обход
class Tree<T>
{
public T Data;
public Tree<T> Left, Right;
…
}
На C#
static void WalkTree<T>(Tree<T> tree, Action <T> func)
{
if (tree == null) return;
WalkTree(tree.Left, func);
WalkTree(tree.Right, func);
func(tree.Data);
}
Заметим, что в последовательной реализации, имеется группа операторов, выполняющих обход обоих ветвей дерева и работающая с текущим узлом. Если наша цель - выполнить это действие для каждого узла в дереве и если порядок, в котором эти узлы будут обслужены неважен, то мы можем распараллелить исполнение группы таких операторов, используя . Параллельная реализация выглядит следующим образом:
static void WalkTree<T>(Tree<T> tree, Action <T> func)
{
if (tree = null) return;
Parallel.Invoke(
() => WalkTree(tree.Left, func);
() => WalkTree(tree.Right, func);
() => func(tree.Data));
}
Только что показанный способ распараллеливания применим и к другим алгоритмам типа "разделяй и властвуй". Рассмотрим последовательную реализацию алгоритма быстрой сортировки:
static void SeqQuickSort<T>(T[] domain, int left, int right)
where T : IComparable<T>
{
if (right - left + 1 <= INSERTION_TRESHOLD)
{
InsertionSort(domain, left, right);
}
else
{
int pivot = Partition(domain, left, right);
SeqQuickSort(domain, left, pivot - 1);
SeqQuickSort(domain, pivot + 1, right);
}
}
Также как в предыдущем примере, :
static void ParQuickSort<T> (T[] domain, int left, int right)
where T : IComparable<T>
{
if (right - left + 1 <= SEQUENTIAL_TRESHOLD)
{
SeqQuickSort (domain, left, right);
}
else
{
int pivot = Partition(domain, left, right);
Parallel.Invoke(
() => SeqQuickSort(domain, left, pivot - 1);
() => SeqQuickSort(domain, pivot + 1, right));
}
}
Заметим, что в последовательной реализации SeqQuickSort, если размер сортируемого сегмента массива достаточно мал, то алгоритм вырождается в алгоритм сортировки вставкой ( InsertionSort ). Для очень больших массивов алгоритм быстрой сортировки значительно эффективнее, чем простые алгоритмы сортировки (SeqQuickSort вырождается в InsertionSort, то в параллельном варианте ParQuickSort выраждается в SeqQuickSort. Аналогичным способом можно организовать только что рассмотренный обход
static void WalkTree<T> (Tree<T> tree, Action<T> func, int depth)
{
if (tree = null) return;
else if (depth > SEQUENTIAL_TRESHOLD)
{
WalkTree(tree.Left, func, depth + 1);
WalkTree(tree.Right, func, depth + 1);
func(tree.Data);
}
else
{
Parallel.Invoke(
() => WalkTree(tree.Left, func, depth + 1);
() => WalkTree(tree.Right, func, depth + 1);
() => func(tree.Data));
}
}
При изучении применения рекурсии в программировании, одним из наиболее типичных примеров является обработка структур данных, представленных в виде дерева. При этом, при обработке деревьев используются различные способы прохождения по их вершинам. Однако, при попытках параллельной (многопоточной) обработки деревьев возникают проблемы, которые отсутствуют при обычной, последовательной обработке.
Рассмотрим простую структуру данных - бинарное дерево ( ):
class Tree<T>
{
public Tree<T> Left, Right; // потомки
public T Data; // данные для этого узла
}
Предположим, что нам необходимо обработать каждую вершину дерева с помощью действия Action<T>, не заботясь о порядке в котором обрабатываются вершины. В последовательном, рекурсивном варианте это сделать очень легко:
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
// Обработать текущую вершину, затем левое поддерево,
// а потом правое
action(tree.Data);
Process(tree.Left, action);
Process(tree.Right, action);
}
Задача 1.
Реализуйте класс , представляющий дерево с произвольной степенью Process.
Нерекурсивный вариант с явным использованием стека (объекта класса ) может выглядеть следующим образом:
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
var toExplore = new Stack<Tree<T> > ();
// Начало обработки с корневой вершины
toExplore.Push(tree);
while (toExplore.Count > 0)
{
// Извлечь очередную вершину, обработать ее, поместить в
// стек ее потомков
var current = toExplore.Pop();
action(current.Data);
if (current.Left != null)
toExplore.Push(current.Left);
if (current.Right != null)
toExplore.Push(current.Right);
}
}
Задача 2.
Переписать приведенный выше метод Process с использованием класса вместо .
Для перехода к параллельному варианту обработки вершин дерева, предположим, что само действие является независимым, вычислительно сложным (т.е.,
С использованием средств .NET Framework, имеются различные возможности для реализации такого параллельного варианта. Ниже приведена реализация, следующая оригинальному рекурсивному алгоритму и использующая класс ThreadPool:
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
// Событие mre используется, чтобы реализовать возврат из
// данного метода только после того, как будет закончена
// обработка вершин-потомков
using (var mre = new ManualResetEvent(false))
{
// Обработать левого потомка асинхронно
ThreadPool.QueueUserWorkItem(delegate
{
Process(tree.Left, action);
mre.Set();
});
// Обработать текущую вершину и правого потомка синхронно
action(tree.Data);
Process(tree.Right, action);
// Ожидание окончания обработки левого потомка
mre.WaitOne();
}
}
Задача 3.
Показать, как аналогичный параллельный вариант можно реализовать, используя класс .
Недостаток предыдущего варианта заключался в том, что обработка правого потомка задерживалась до тех пор, пока не будут обработаны данные текущей вершины. Чтобы повысить степень
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
// Необходимо воспользоваться конструкцией события
// для ожидания завершения обработки потомков
using (var mre = new ManualResetEvent(false))
{
int count = 2;
// Обработать левого потомка асинхронно
ThreadPool.QueueUserWorkItem(delegate
{
Process(tree.Left, action);
if (Interlocked.Decrement(ref count) == 0)
mre.Set();
});
// Обработать правого потомка асинхронно
ThreadPool.QueueUserWorkItem(delegate
{
Process(tree.Right, action);
if (Interlocked.Decrement(ref count) == 0)
mre.Set();
});
// Обработать текущую вершину синхронно
action(tree.Data);
// Ожидание завершения обработки потомков
mre.WaitOne();
}
}
Задача 4.
Переписать приведенный выше метод Process в предположении, что класс определен так, как в Задаче 1. Подготовьте несколько объектов класса с количеством вершин, соответственно, 100, 200, 500, 1000, и протестируйте метод Process на данных деревьях.
В Лекции 2 были рассмотрены параллельные реализации циклов For и ForEach. Еще один способ распараллеливания, поддерживаемый классом - это метод .
Статический метод позволяет распараллелить исполнение
Подобные ситуации часто возникают в рекурсивных алгоритмах и алгоритмах типа "разделяй и властвуй". Рассмотрим, например, обход
class Tree<T>
{
public T Data;
public Tree<T> Left, Right;
…
}
На C#
static void WalkTree<T>(Tree<T> tree, Action <T> func)
{
if (tree == null) return;
WalkTree(tree.Left, func);
WalkTree(tree.Right, func);
func(tree.Data);
}
Заметим, что в последовательной реализации, имеется группа операторов, выполняющих обход обоих ветвей дерева и работающая с текущим узлом. Если наша цель - выполнить это действие для каждого узла в дереве и если порядок, в котором эти узлы будут обслужены неважен, то мы можем распараллелить исполнение группы таких операторов, используя . Параллельная реализация выглядит следующим образом:
static void WalkTree<T>(Tree<T> tree, Action <T> func)
{
if (tree = null) return;
Parallel.Invoke(
() => WalkTree(tree.Left, func);
() => WalkTree(tree.Right, func);
() => func(tree.Data));
}
Только что показанный способ распараллеливания применим и к другим алгоритмам типа "разделяй и властвуй". Рассмотрим последовательную реализацию алгоритма быстрой сортировки:
static void SeqQuickSort<T>(T[] domain, int left, int right)
where T : IComparable<T>
{
if (right - left + 1 <= INSERTION_TRESHOLD)
{
InsertionSort(domain, left, right);
}
else
{
int pivot = Partition(domain, left, right);
SeqQuickSort(domain, left, pivot - 1);
SeqQuickSort(domain, pivot + 1, right);
}
}
Также как в предыдущем примере, :
static void ParQuickSort<T> (T[] domain, int left, int right)
where T : IComparable<T>
{
if (right - left + 1 <= SEQUENTIAL_TRESHOLD)
{
SeqQuickSort (domain, left, right);
}
else
{
int pivot = Partition(domain, left, right);
Parallel.Invoke(
() => SeqQuickSort(domain, left, pivot - 1);
() => SeqQuickSort(domain, pivot + 1, right));
}
}
Заметим, что в последовательной реализации SeqQuickSort, если размер сортируемого сегмента массива достаточно мал, то алгоритм вырождается в алгоритм сортировки вставкой ( InsertionSort ). Для очень больших массивов алгоритм быстрой сортировки значительно эффективнее, чем простые алгоритмы сортировки (SeqQuickSort вырождается в InsertionSort, то в параллельном варианте ParQuickSort выраждается в SeqQuickSort. Аналогичным способом можно организовать только что рассмотренный обход
static void WalkTree<T> (Tree<T> tree, Action<T> func, int depth)
{
if (tree = null) return;
else if (depth > SEQUENTIAL_TRESHOLD)
{
WalkTree(tree.Left, func, depth + 1);
WalkTree(tree.Right, func, depth + 1);
func(tree.Data);
}
else
{
Parallel.Invoke(
() => WalkTree(tree.Left, func, depth + 1);
() => WalkTree(tree.Right, func, depth + 1);
() => func(tree.Data));
}
}
При изучении применения рекурсии в программировании, одним из наиболее типичных примеров является обработка структур данных, представленных в виде дерева. При этом, при обработке деревьев используются различные способы прохождения по их вершинам. Однако, при попытках параллельной (многопоточной) обработки деревьев возникают проблемы, которые отсутствуют при обычной, последовательной обработке.
Рассмотрим простую структуру данных - бинарное дерево ( ):
class Tree<T>
{
public Tree<T> Left, Right; // потомки
public T Data; // данные для этого узла
}
Предположим, что нам необходимо обработать каждую вершину дерева с помощью действия Action<T>, не заботясь о порядке в котором обрабатываются вершины. В последовательном, рекурсивном варианте это сделать очень легко:
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
// Обработать текущую вершину, затем левое поддерево,
// а потом правое
action(tree.Data);
Process(tree.Left, action);
Process(tree.Right, action);
}
Задача 1.
Реализуйте класс , представляющий дерево с произвольной степенью Process.
Нерекурсивный вариант с явным использованием стека (объекта класса ) может выглядеть следующим образом:
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
var toExplore = new Stack<Tree<T> > ();
// Начало обработки с корневой вершины
toExplore.Push(tree);
while (toExplore.Count > 0)
{
// Извлечь очередную вершину, обработать ее, поместить в
// стек ее потомков
var current = toExplore.Pop();
action(current.Data);
if (current.Left != null)
toExplore.Push(current.Left);
if (current.Right != null)
toExplore.Push(current.Right);
}
}
Задача 2.
Переписать приведенный выше метод Process с использованием класса вместо .
Для перехода к параллельному варианту обработки вершин дерева, предположим, что само действие является независимым, вычислительно сложным (т.е.,
С использованием средств .NET Framework, имеются различные возможности для реализации такого параллельного варианта. Ниже приведена реализация, следующая оригинальному рекурсивному алгоритму и использующая класс ThreadPool:
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
// Событие mre используется, чтобы реализовать возврат из
// данного метода только после того, как будет закончена
// обработка вершин-потомков
using (var mre = new ManualResetEvent(false))
{
// Обработать левого потомка асинхронно
ThreadPool.QueueUserWorkItem(delegate
{
Process(tree.Left, action);
mre.Set();
});
// Обработать текущую вершину и правого потомка синхронно
action(tree.Data);
Process(tree.Right, action);
// Ожидание окончания обработки левого потомка
mre.WaitOne();
}
}
Задача 3.
Показать, как аналогичный параллельный вариант можно реализовать, используя класс .
Недостаток предыдущего варианта заключался в том, что обработка правого потомка задерживалась до тех пор, пока не будут обработаны данные текущей вершины. Чтобы повысить степень
public static void Process<T> (Tree<T> tree, Action<T> action)
{
if (tree == null) return;
// Необходимо воспользоваться конструкцией события
// для ожидания завершения обработки потомков
using (var mre = new ManualResetEvent(false))
{
int count = 2;
// Обработать левого потомка асинхронно
ThreadPool.QueueUserWorkItem(delegate
{
Process(tree.Left, action);
if (Interlocked.Decrement(ref count) == 0)
mre.Set();
});
// Обработать правого потомка асинхронно
ThreadPool.QueueUserWorkItem(delegate
{
Process(tree.Right, action);
if (Interlocked.Decrement(ref count) == 0)
mre.Set();
});
// Обработать текущую вершину синхронно
action(tree.Data);
// Ожидание завершения обработки потомков
mre.WaitOne();
}
}
Задача 4.
Переписать приведенный выше метод Process в предположении, что класс определен так, как в Задаче 1. Подготовьте несколько объектов класса с количеством вершин, соответственно, 100, 200, 500, 1000, и протестируйте метод Process на данных деревьях.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.