Опубликован: 23.04.2013 | Доступ: свободный | Студентов: 854 / 184 | Длительность: 12:54:00
Лекция 8:

Распараллеливание циклов. Класс Parallel

< Лекция 7 || Лекция 8: 123456 || Лекция 9 >

Оператор 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);
        }
    }

Осталось привести результаты работы:

Обработка коллекции роботов

Рис. 7.14. Обработка коллекции роботов

Об одной "классической" ошибке при параллельном программировании

К приведенному выше проекту можно высказать несколько упреков, указав на характерные ошибки программирования. Вот некоторые из них:

  • Интерфейс не отделен от бизнес-логики, - в методе Init результаты выводятся на консоль, а не сохраняются, как положено в полях класса или в возвращаемом значении.
  • Для public переменных не всегда даются документируемые комментарии.
  • Память используется не эффективно, поскольку дублируются коллекции robots и results.

Можно указать и на другие ошибки, нарушающие стиль программирования. Я иногда сознательно иду на подобные нарушения для краткости текста и ясности изложения. Но на одной ошибке, которую я сделал в ходе разработки проекта, хочу остановиться подробнее, поскольку, полагаю, она является типичной ошибкой тех, кто начинает работать с параллельными программами. В методах Init_Robots и CAV мне понадобилось закрывать критическую секцию объектом locker:

lock(locker){<критическая секция>}

Где нужно объявлять объект locker? Создавая методы Init_Robots и CAV, я там же объявил и объект locker, представляющий ключ, закрывающий секцию. Когда я начал отладку проекта, то для небольших значений n все работало прекрасно. Но при параллельных вычислениях отладка на "малых" примерах, широко применяемая в последовательном программировании, мало что дает, - ошибки проявляются на "больших" данных. Уже при n, больших 100, из-за гонки данных стали теряться элементы коллекции. Причина в том, что объявленный ключ представлял локальную переменную. В результате каждый поток открывал критическую секцию своим ключом, что и приводило к гонке данных.

Ключ должен быть глобальной переменной - полем класса, как это сделано в нашем проекте. Помните об этом.

< Лекция 7 || Лекция 8: 123456 || Лекция 9 >
Алексей Рыжков
Алексей Рыжков

не хватает одного параметра:

static void Main(string[] args)
        {
            x = new int[n];
            Print(Sample1,"original");
            Print(Sample1P, "paralel");
            Console.Read();
        }

Никита Белов
Никита Белов

Выставил оценки курса и заданий, начал писать замечания. После нажатия кнопки "Enter" окно отзыва пропало, открыть его снова не могу. Кнопка "Удалить комментарий" в разделе "Мнения" не работает. Как мне отредактировать недописанный отзыв?