Опубликован: 23.04.2013 | Уровень: для всех | Доступ: платный
Лекция 7:

Пул потоков и библиотека параллельных задач

< Лекция 6 || Лекция 7: 123 || Лекция 8 >

Библиотека параллельных задач

Библиотека параллельных задач - TPL (Task Parallel Library), представленная в четвертой версии .Net Framework 4.0, позволяет справиться как с проблемой синхронизации задач, выполняемых в пуле потоков, так и предоставить много новых дополнительных возможностей. На сегодняшний день лучший способ создания многопоточного приложения предполагает работу с объектами библиотеки TPL.

Понятие "задача" является одним из центральных понятий в параллельном программировании. Распараллеливание "по задачам" и распараллеливание "по данным" - два основных принципа параллельных вычислений. Вполне естественно введение класса Task, объекты которого представляют задачи. В .Net Framework 4.0 в пространстве имен Threading выделено пространство Threading.Tasks, содержащее как класс Task, так и другие классы, поддерживающие работу с задачами, возможности их параллельного выполнения. Эти классы, являясь надстройкой над пулом потоков, позволяют полностью абстрагироваться от "потоков" - низкоуровневых механизмов и сосредоточиться на работе с объектами более высокого уровня - "задачами", отражающими суть приложения. Тем не менее, корректная работа с задачами предполагает, а на самом деле невозможна без понимания всех проблем, присущих параллельным вычислениям - синхронизации, блокировкам, гонки данных и клинчам.

Прежде чем формально описать возможности класса Task и других классов пространства System.Threading.Tasks, давайте модифицируем наш предыдущий пример с несколькими работниками, выразив его в терминах задач, не обращаясь явно к пулу потоков. Класс ThreadingPool и пул потоков теперь неявно будут присутствовать "за сценой", а на сцене будут выступать "задачи". С помощью новых актеров постараемся избавиться от недостатков, присущих предыдущему примеру.

Методы, подлежащие выполнению, будем теперь связывать не с потоками, а с задачами. Требования к методам остаются прежними. Методы, как и ранее, могут быть трех типов. Это может быть метод с фиксированной сигнатурой void (object). Это может быть метод с такой же сигнатурой, вызываемый объектом некоторого класса. Это может быть анонимный метод с такой же сигнатурой, способный вызвать метод с произвольной сигнатурой. Все эти возможности были продемонстрированы в предыдущем примере. Слегка модифицируя наш пример, реализуем эти три способа, оперируя уже с задачами. Начнем с анонимного метода. В новом проекте добавим в процедуру Main следующий фрагмент кода:

// Создание первой задачи task1
            //С задачей связывается анонимный метод,
            //вызывающий метод с произвольной сигнатурой
            string res ="";
            Task task1 = new Task((object inf) =>
                { WorkerOne("Дмитрий", 33, inf, out res ); },
                "отличный работник");
            //Запуск задачи и ожидание ее завершения
            task1.Start();
            task1.Wait();
            //Печать результатов работы метода в основном потоке
            Console.WriteLine(res);

Метод WorkerOne, вызываемый анонимным методом, может иметь произвольную сигнатуру. В момент создания ему можно передать фактические параметры. Заметьте, в нашем примере методу также передается значение параметра inf - единственного параметра анонимного метода. Метод WorkerOne теперь не будет выводить результаты своей работы на консоль. Он, как и положено добропорядочному методу, имеет выходной параметр res, содержащий результат работы метода.

В целом схема работы с задачей проста и естественна. Создается объект task1, характеризующий задачу. В момент создания с ним связывается метод, который должна выполнить задача. Затем задача стартует. Основной поток в этот момент приостанавливается, ожидая завершения задачи. Когда это событие происходит, основной поток продолжает свое выполнение, выводя на консоль результаты решения задачи task1.

Приведу текст метода WorkerOne:

static void WorkerOne(string name, int age, object inf, out string res)
        {
            res = string.Format("Я первый работник. Имя: {0}," +
            "возраст: {1} , рекомендация: {2}", name, age, inf);
        }

Рассмотрим теперь вторую возможность - передачу задаче метода с фиксированной сигнатурой void (object). При создании задачи конструктору класса Task передаются два параметра. Первый параметр функционального типа, задаваемый делегатом Action, несет информацию о методе, второй параметр типа object передает методу всю входную информацию, необходимую для работы метода.

При работе с задачей task1 мы не стали явно создавать объект класса Action. Теперь же явно создадим этот объект, что хотя и не обязательно, но облегчает понимание текста программы. Вот как выглядит следующий фрагмент кода, добавляемый в процедуру Main:

Console.WriteLine("Задача task1 отработала. Стартуют задачи task2 и task3!");
      //Создание объектов: action2, info, task2
      //action2 - исполняемый метод с сигнатурой void (object)
      //info - глобальный объект - содержит информацию, передаваемую методу
      // task2 - задача, которой передается action2 и info          
            Action<object> action2 = WorkerTwo; 
            info = new Info("Феликс", 50);            
            Task task2 = new Task(action2, info);

Параметр action2 задает выполняемый метод, info - информацию, передаваемую методу. Класс Info, определяющий объект info, задается следующей структурой:

struct Info
        {
            string name;
            int age;
            string result ;
            public Info(string name, int age)
            {
                this.name = name;
                this.age = age;
                result = "";
            }
            public string Name
            {
                get { return name; }
            }
            public int Age
            {
                get { return age; }
            }
            public string Result
            {
                set { result = value; }
                get { return result; }
            }
        }

Параметры name и age содержательно являются входными параметрами, result - выходной параметр.

Метод WorkerTwo выглядит следующим образом:

static void WorkerTwo(object inf)
        {
            info.Result = "Я работник второй! " + "Меня зовут " +
            ((Info)inf).Name + " Мне " + ((Info)inf).Age.ToString();
        }

Следует обратить внимание на одно важное обстоятельство. Цель, которую мы поставили, состоит в том, чтобы метод WorkerTwo результат своей работы сохранял в поле result структуры Info, передаваемой методу в параметре inf. Но беда в том, что параметр типа object может передать методу входную информацию, но не может передать во внешний мир изменения, сделанные в полях объекта. Все изменения носят локальный характер, доступны только в пределах метода и исчезают, когда тело заканчивает свою работу. Поэтому метод для сохранения результатов своей работы использует глобальный объект info, определенный в классе Program, содержащем как метод Main, так и метод WorkerTwo:

static Info info;

Обратите внимание, входные данные, нужные методу WorkerTwo, берутся из объекта inf, а результат записывается в поле result глобального объекта info.

Прежде чем запустить задачу task2 на выполнение, создадим еще одну задачу task3, в которой присоединяемый к задаче метод будет вызываться объектом sim класса Simple. Добавим в Main следующий фрагмент кода:

///Создание объекта sim класса Simple
            ///При создании задачи task3 ей передается 
            ///объект sim, вызывающий метод About
            ///и информация, передаваемая методу
            Simple sim = new Simple("Виктор", 27, "программист");
            Task task3 = new Task(sim.About, "супер компьютерщик");

Класс Simple устроен следующим образом:

class Simple
        {
            string name;
            int age;
            string profession;
            string result;

            public Simple(string name, int age, string profession)
            {
                this.name = name;
                this.age = age;
                this.profession = profession;
            }
            /// <summary>
            /// Метод About этого класса при формировании результата
            /// использует информацию из полей класса
            /// и дополнительную информацию, передаваемую в объекте inf
            /// </summary>
            public void About(object inf)
            {
                result = string.Format("Новый работник. Мое имя - {0}, " +
                    " возраст - {1} профессия - {2}",
                    name, age, profession);
                if (inf != null)
                   result += "  " + inf.ToString();
            }
            public string Result
            {
                get { return result; }
            }
        }

Метод About, выполняемый задачей task3, формирует результат работы в поле result класса Simple. Задачи task2 и task3 созданы в основном потоке, но еще не запущены. Запустим их на выполнение, прервав основной поток до завершения работы задач:

///Задачи task2 и task3 стартуют одновременно
            task2.Start();
            task3.Start();
            ///основной поток приостанавливается
            ///ожидая завершения работы задач
            //Task.WaitAll();
            task2.Wait();
            task3.Wait();

Замечу, что метод WaitAll класса Task применим к массиву задач, но не к последовательности задач. Поэтому в примере он присутствует в закомментированном виде и включено явное ожидание завершения для каждой задачи в отдельности. По завершении работы задач основной поток возобновляет свою работу и выводит на консоль результаты работы:

///основной поток возобновляет работу
            ///дождавшись завершения задач
            ///выводит на консоль результаты работы задач
            Console.WriteLine("Задача task2 отработала.");
            Console.WriteLine(info.Result);
            Console.WriteLine("Задача task3 отработала.");
            Console.WriteLine(sim.Result);

Осталось взглянуть на результаты проделанной работы:

Результаты работы параллельно исполняемых задач

увеличить изображение
Рис. 6.2. Результаты работы параллельно исполняемых задач

Подведем первые итоги работы с библиотекой TPL. Нам удалось создать несколько задач, используя разные приемы при их создании. Задачи просто запускаются на параллельное выполнение. При этом нам нет необходимости думать о числе процессоров компьютера, о потоках операционной системы. Вся работа идет на высоком абстрактном уровне объектов, отвечающих сути нашего приложения. Вопросы синхронизации работы задач решаются просто. Результаты решения выводятся на консоль в нужном порядке. Проблемы, обсуждаемые при построении первого примера, успешно разрешены, так что можно двигаться дальше.

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

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

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

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

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

Анатолий Кирсанов
Анатолий Кирсанов
Россия, Тамбов, Российский Новый Университет, 2012
Алексей Горячих
Алексей Горячих
Россия