не хватает одного параметра: static void Main(string[] args) |
Распараллеливание циклов. Класс Parallel
Оператор Parallel.ForEach
Принципиально, все, что было сказано о распараллеливании циклов с использованием оператора Parallel.For, относится и к оператору (методу) Parallel.ForEach. Разница такая же, как и между обычными операторами for и foreach. Оператор ForEach позволяет распараллелить обработку элементов некоторой коллекции - массивов, списков, словарей, - предоставляя возможность обработки каждого элемента в отдельном потоке.
Для оператора Parallel.ForEach сохраняется ограничение, характерное для его прототипа - обычного оператора foreach, - элемент коллекции можно использовать только для чтения, но не для его изменения. Оператор в некотором порядке предоставляет элемент за элементом из коллекции. Если быть точным, то элементы выбираются из некоторого буфера, создаваемого при работе с коллекцией. Предоставляемый элемент программист может изменять, но эти изменения никак не отразятся на элементах самой коллекции, поскольку предоставляется локальный объект и все изменения носят локальный характер. При завершении метода локальный элемент перестает существовать, и все изменения пропадают вместе с самим элементом. Ситуация аналогична передаче методу параметра значимого типа, заданного без описателя ref или out. Для такого входного параметра создается локальная копия, существующая только на время выполнения метода.
В цикле Parallel.ForEach можно создавать элементы новой коллекции, но нельзя модифицировать коллекцию, предоставляемую оператором цикла.
Метод Parallel.ForEach перегружен, мы ограничимся рассмотрением его простейшей версии, имеющей следующий синтаксис:
public static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source, Action<TSource> body)
Метод представляет функцию, возвращающую тот же результат, что и метод Parallel.For.
У метода в этой реализации два аргумента, - первый представляет коллекцию, по элементам которой выполняется цикл, второй - метод, которому представляется элемент коллекции.
Корректная работа предполагает, что итерации независимы, так что обработка каждого элемента коллекции может выполняться независимо, параллельно и в любом порядке.
Давайте построим некоторый модельный пример на работу с коллекцией. Пусть у нас есть некоторая коллекция роботов, каждый из которых обладает некоторым набором характеристик. Для каждого из роботов необходимо обработать эти характеристики и вынести заключение, позволяющее отобрать кандидата для выполнения некоторого задания.
Начнем с создания класса Robot. Вот общая часть этого класса:
/// <summary> /// Класс, моделирующий работу с коллекцией роботов /// </summary> public class Robots { //число роботов int n; //число характеристик робота int m; //коллекция роботов public List<StructR> robots; //коллекция,создаваемая при обработке public List<StructR> results; //глобальный ключ закрытия критической секции object locker = new object(); Random rnd = new Random(); /// <summary> /// Конструктор /// </summary> /// <param name="n">число роботов</param> public Robots(int n) { this.n = n; m = 5; robots = new List<StructR>(n); results = new List<StructR>(n); }
Пояснения смысла полей класса даны в комментариях к проекту. Коллекция роботов задана списком, элементы которого представляют структуру StructR, характеризующую робота. В нашей модели она проста:
/// <summary> /// Характеристика робота /// </summary> public struct StructR { string id; int[] marks; double average_ball; public string Id { get { return id; } set { id = value; } } public int[] Marks { get { return marks; } set { marks = value; } } public double Average_ball { get { return average_ball; } set { average_ball = value; } } }
Здесь, поле id идентифицирует робота, marks - это его характеристики, а average_ball - это характеристика, которую необходимо вычислить в результате обработки оценок marks.
Добавим теперь в класс Robot метод Init, позволяющий моделировать создание коллекции. Поскольку создание каждого робота можно вести независимо, то используем распараллеливание и в работе этого метода:
/// <summary> /// Инициализация списка robots /// </summary> public void Init() { try { ParallelLoopResult res = Parallel.For(0, n, Init_Robots); if (!res.IsCompleted) Console.WriteLine("Ошибки при инициализации списка"); else Console.WriteLine("все итерации завершились нормально"); } catch (AggregateException ae) { Console.WriteLine(ae.Message); ae.Handle((x) => { Console.WriteLine(x.Message); return true; }); } }
Заметьте, на этапе разработки проекта лучше использовать полную форму с получением результата работы цикла, с обработкой возможных исключительных ситуаций. Затем, когда проект отлажен, будучи уверенным в корректности инициализации, можно обойтись одной строчкой:
Parallel.For(0, n, Init_Robots);
Метод Init_Robots - это метод, выполняемый на каждой итерации, которому передается индекс итерации цикла:
/// <summary> /// Создание робота /// </summary> /// <param name="k">индекс итерации цикла /// В методе не используется</param> void Init_Robots (int k) { StructR sr = new StructR(); string id = "R" + rnd.Next(1, 1000); int[] marks = new int[m]; for (int i = 0; i < m; i++) { marks[i] = rnd.Next(5, 25); } sr.Id = id; sr.Marks = marks; lock (locker) { robots.Add(sr); } }
Здесь создается объект sr типа StructR и этот объект, моделирующий робота, добавляется в коллекцию (список) роботов. Поскольку список является общим ресурсом, то добавление нового элемента коллекции помещается в критическую секцию, закрываемую ключом locker.
Добавим теперь в класс Robot метод, позволяющий проводить параллельную обработку созданной коллекции. В этом методе используем конструкцию Parallel.For:
/// <summary> /// Обработка коллекции robots /// </summary> public void CountAverage() { Parallel.ForEach(robots, CAV); }
Для ясности понимания вызова я не стал приводить полную форму с получением результата и обработкой возможной исключительной ситуации. Она такая же, как и для оператора Parallel.For.
Итак, мы видим, что при вызове метода Parallel.ForEach ему передается коллекция robots и метод CAV, обрабатывающий элемент коллекции. Вот как выглядит этот метод в нашем случае:
void CAV(StructR sr) { sr.Average_ball = 0; for (int i = 0; i < m; i++) sr.Average_ball += sr.Marks[i]; sr.Average_ball = Math.Round(sr.Average_ball / m, 2); lock (locker) { results.Add(sr); } }
Наша цель состоит в том, чтобы изменить значения поля в структуре, характеризующей робота. Конечно, хотелось бы, чтобы значение поля sr.Average_ball, вычисляемое в цикле, изменялось бы непосредственно для каждого элемента обрабатываемой коллекции robots, но, как уже говорилось, конструкция ForEach этого не позволяет. Поэтому в методе создается новая коллекция results, аналогичная коллекции robots, отличающаяся заполненным полем Average_ball. И здесь, критическая секция, в которой ведется работа с общим ресурсом, закрывается ключом locker.
В завершение нашей модели добавим еще один метод, позволяющий выбрать одного из роботов коллекции. В данном случае "лучшим" будем признавать робота с наибольшим баллом Average_ball. Хотя вычисление максимума также может быть распараллелено, о чем говорилось в предыдущих главах, но мы ограничимся последовательным алгоритмом, в котором используется обычный оператор foreach:
/// <summary> /// Нахождение лучшего /// </summary> /// <returns>робот с максимальным баллом</returns> public StructR Best() { StructR best = results.ElementAt(0); foreach (StructR item in results) if (item.Average_ball > best.Average_ball) best = item; return best; }
Приведу теперь процедуру Main консольного проекта, оживляющую нашу модель и приводящую ее в действие:
class Program { static int n = 15; static Robots my = new Robots(n); static void Main(string[] args) { my.Init(); my.CountAverage(); PrintRobots(); StructR best = my.Best(); Console.WriteLine("Лучший робот"); PrintOne(best); } static void PrintRobots() { StructR item; if( n < 20) for (int i = 0; i < n; i++) { item = my.results.ElementAt(i); PrintOne(item); } } static void PrintOne(StructR one) { Console.WriteLine("ID : {0} Оценка : {1}", one.Id, one.Average_ball); } }
Осталось привести результаты работы:
Об одной "классической" ошибке при параллельном программировании
К приведенному выше проекту можно высказать несколько упреков, указав на характерные ошибки программирования. Вот некоторые из них:
- Интерфейс не отделен от бизнес-логики, - в методе Init результаты выводятся на консоль, а не сохраняются, как положено в полях класса или в возвращаемом значении.
- Для public переменных не всегда даются документируемые комментарии.
- Память используется не эффективно, поскольку дублируются коллекции robots и results.
Можно указать и на другие ошибки, нарушающие стиль программирования. Я иногда сознательно иду на подобные нарушения для краткости текста и ясности изложения. Но на одной ошибке, которую я сделал в ходе разработки проекта, хочу остановиться подробнее, поскольку, полагаю, она является типичной ошибкой тех, кто начинает работать с параллельными программами. В методах Init_Robots и CAV мне понадобилось закрывать критическую секцию объектом locker:
lock(locker){<критическая секция>}
Где нужно объявлять объект locker? Создавая методы Init_Robots и CAV, я там же объявил и объект locker, представляющий ключ, закрывающий секцию. Когда я начал отладку проекта, то для небольших значений n все работало прекрасно. Но при параллельных вычислениях отладка на "малых" примерах, широко применяемая в последовательном программировании, мало что дает, - ошибки проявляются на "больших" данных. Уже при n, больших 100, из-за гонки данных стали теряться элементы коллекции. Причина в том, что объявленный ключ представлял локальную переменную. В результате каждый поток открывал критическую секцию своим ключом, что и приводило к гонке данных.
Ключ должен быть глобальной переменной - полем класса, как это сделано в нашем проекте. Помните об этом.