не хватает одного параметра: static void Main(string[] args) |
Пул потоков и библиотека параллельных задач
Библиотека параллельных задач
Библиотека параллельных задач - 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);
Осталось взглянуть на результаты проделанной работы:
Подведем первые итоги работы с библиотекой TPL. Нам удалось создать несколько задач, используя разные приемы при их создании. Задачи просто запускаются на параллельное выполнение. При этом нам нет необходимости думать о числе процессоров компьютера, о потоках операционной системы. Вся работа идет на высоком абстрактном уровне объектов, отвечающих сути нашего приложения. Вопросы синхронизации работы задач решаются просто. Результаты решения выводятся на консоль в нужном порядке. Проблемы, обсуждаемые при построении первого примера, успешно разрешены, так что можно двигаться дальше.