Типовые модели параллельных приложений
Модель с равноправными узлами
В модели с равноправными узлами все потоки (или задачи) участвуют в обработке; центрального узла нет. Работа узлов может осуществляться параллельно. Задачи, в которых декомпозиция осуществляется по данным, являются примером модели с равноправными узлами. Для реализации алгоритмов с параллелизмом по данным можно использовать средства TPL, автоматически осуществляющие декомпозицию и агрегацию результатов с учетом возможностей вычислительной системы и текущей загруженностью. В эту группу входят шаблоны Parallel.For, Parallel.ForEach, а также технология PLINQ.
В следующем примере рассматривается матричное умножение с декомпозицией по строкам первой матрицы. Каждый рабочий поток (подзадача) оперирует с одной или несколькими строками первой матрицы и всей второй матрицей. Таким образом, каждый поток вычисляет соответствующие строки результирующей матрицы. Для эффективного разделения строк матрицы можно использовать шаблон Parallel.For.
Parallel.For(0, N, i => { for(int j=0; j<N; j++) for(int k=0; k<N; k++) C[i,j] += A[i,k] * B[k, j]; });
В модели с равноправными узлами работа каждого участника может включать несколько последовательных этапов. Примером многоэтапной обработки является шаблон Map/Reduce. Обработка элементов включает следующие этапы:
- Map. Обработка элементов, формирование для каждого элементы пары "ключ-значение", группировка элементов по ключам.
- Reduce. Обработка сгруппированных элементов и выполнение для каждой группы заданной редукции.
Примером шаблона Map/Reduce является задача подсчёта встречаемости слов в тексте. Операция Map создает для каждого уникального слова пару "ключ-значение", ключ соответствует слову, значение равно единице. Все слова группируются. Операция Reduce вычисляет количество слов в каждой группе.
// Операция Map // Генерируем пары ключ-значение (word, 1) ILookup<string, int> map = words.AsParallel().ToLookup(p => p, k => 1); // Операция Reduce // Вычисляем встречаемость слов // Отбираем с частотой встречаемости больше 1 var reduce = from IGrouping<string, int> wordMap in map.AsParallel() where wordMap.Count() > 1 select new { Word = wordMap.Key, Count = wordMap.Count() }; // Отображение результатов foreach (var word in reduce) Console.WriteLine("Word: ‘{0}’; Count: {1}", word.Word, word.Count); Console.ReadLine();
Для повышения эффективности алгоритм можно переписать в виде одного PLINQ-запроса:
var files = Directory.EnumerateFiles(dirPath, "*.txt").AsParallel(); var counts = files .SelectMany(f => File.ReadLines(f).SelectMany(line => line.Split(delimeters))) .GroupBy(w => w) .Select(g => new {Word = g.Key, Count = g.Count()});
В первой строке инициализируется перечислимый список файлов с расширением *.txt в директории dirPath. Список файлов представляет собой тип ParallelQuery<File>, поэтому все запросы выполняются параллельно. Первый запрос SelectMany формирует общий список слов из всех файлов. Для разделения строк файла на слова используется массив разделителей delimeters. Оператор GroupBy осуществляет группировку слов, последний оператор Select для каждой группы формирует безымянный тип с полями Word и Count.
Модель конвейера
В модели конвейерной обработки (pipelines) поток обрабатываемых данных проходит через несколько этапов. Прохождение этапов осуществляется строго последовательно. Параллелизм достигается за счет одновременной обработки разных элементов на разных этапах.
Если последовательность элементов заранее определена и порядок обработки элементов не важен, то для распараллеливания эффективнее использовать модель с равноправными узлами (шаблон MapReduce, шаблон Parallel.For). Конвейерная обработка возникает при работе с последовательными потоками данных (потоки событий, потоки видеосигналов, потоки изображений), а также при многоэтапной обработке элементов последовательности в строго заданном порядке.
Рассмотрим конвейер, обрабатывающий документы. Обработка документов включает следующие этапы: загрузка документа (из файла, из почтового сервера и т.д.), проверка правописания, форматирование, отправка документа. Обработка документов выполняется строго последовательно, то есть первым отправляется тот документ, который загружен первым. Но при организации конвейера обработка разных документов на разных этапах может осуществляться параллельно.
Реализацию конвейерной обработки можно реализовать с помощью задач и конкурентных очередей. Каждая задача реализует этап конвейера, очереди выступают буферами, накапливающими элементы.
Последовательный алгоритм обработки изображений выглядит следующим образом:
while(bWorking) { doc = LoadDoc(..); spelledDoc = CheckSpelling(doc); formattedDoc = FormatDoc (spelledDoc); Send(formattedDoc); nSlide++; }
Для реализации конвейера необходимо ввести буферы, предназначенные для параллельной работы, и оформить этапы в виде асинхронных задач:
var downloads = new BlockingCollection<MyDoc>(limit); var checked = new BlockingCollection<MyDoc>(limit); var formatted = new BlockingCollection<MyDoc>(limit); var factory = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None); var loadTask = factory.StartNew(() => LoadingDocs(downloads)); var checkTask = factory.StartNew(() => CheckingDocs(downloads, checked)); var filterTask = factory.StartNew(() => FilteringDocs(checked, filtered)); var sendTask = factory.StartNew(() => SendingDocs(formatted.GetConsumingEnumerable())); Task.WaitAll(loadTask, checkTask, filterTask, sendTask);
Задачи создаются с параметром LongRunning, чтобы с каждым этапом был связан собственный поток, и планировщик не оценивал производительность выполнения задач. Задачи читают элементы из одной очереди и пишут в другую. Если во входной очереди нет элементов, то поток блокируется для ожидания поступления элементов. Если выходная очередь переполнена, то поток, осуществляющий запись, блокируется в ожидании извлечения, хотя бы одного элемента.