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