|
не хватает одного параметра: 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, из-за гонки данных стали теряться элементы коллекции. Причина в том, что объявленный ключ представлял локальную переменную. В результате каждый поток открывал критическую секцию своим ключом, что и приводило к гонке данных.
Ключ должен быть глобальной переменной - полем класса, как это сделано в нашем проекте. Помните об этом.
