не хватает одного параметра: static void Main(string[] args) |
Пул потоков и библиотека параллельных задач
Класс ThreadPool
Для работы с потоками в пространстве имен Threading есть много полезных классов. Центральную роль играет класс Thread, объекты которого представляют отдельные потоки. Две предыдущие главы были посвящены работе с объектами этого класса. В этой главе рассмотрим еще один весьма полезный и часто используемый в приложениях класс ThreadPool. В отличие от класса Thread класс ThreadPool является статическим классом. Объект этого класса существует в единственном экземпляре, имя его совпадает с именем класса. Объект создается системой автоматически и предоставляется программисту для использования. Этот единственный объект описывает пул потоков - некоторое множество потоков. Управляет потоками операционная система. Программист не может ни создать пул потоков, ни удалить пул, не может добавить поток в пул, не может удалить ни один из потоков, входящих в пул.
Что же тогда может программист, что дает ему пул потоков? Он может стать в очередь, записавшись на использование одного из потоков пула для выполнения своей задачи. Задача, выполняемая потоком, представляет метод с фиксированной сигнатурой.
Когда в пуле потоков есть свободные потоки, а в очереди есть задачи на выполнение, то задача в порядке очереди связывается с потоком и начинает выполняться в соответствии с общей стратегией выполнения потоков. По завершении задачи, поток не уничтожается, а возвращается в пул потоков. Поток из пула не только никогда не удаляется, он никогда и не завершается, всегда существует, время от времени исполняя различные задачи. Как следствие, поток, поставивший задачу в очередь, не может узнать о ее завершении, используя метод Join, как это делалось для синхронизации работы потоков - объектов класса Thread. Здесь для синхронизации нужны другие механизмы.
Когда программисту для решения его задачи нужно создать один - два потока, то это может быть приемлемым решением. Но когда требуется создавать множество потоков, то накладные расходы, связанные с созданием потока и его последующим удалением, становятся непомерными и могут уничтожить все преимущества, предоставляемые распараллеливанием вычислений. В этих случаях работа с пулом потоков в том или ином виде становится совершенно необходимым делом.
Потоки из пула выполняются в фоновом режиме. Поэтому их работа должна быть завершена до окончания работы потока, выполняемого в режиме переднего плана. В противном случае по завершении работы основного потока завершится и выполнение потоков фона.
Требования к методу, передаваемому в пул потоков
Метод, передаваемый потоку, отвечает обычным требованиям. Во-первых, это может быть void метод с одним параметром типа object, - метод, принадлежащий классу, задаваемым делегатом WaitCallback.
Если методу не нужно передавать информацию, то в момент вызова фактический параметр, соответствующий параметру obj, просто не задается. Если же нужно передать информацию, то фактический параметр может быть объектом, содержащим всю необходимую информацию. Естественно, что в этом случае в методе, прежде чем использовать информацию, необходимо привести параметр obj к требуемому типу.
Еще одна возможность состоит в том, чтобы передать потоку не просто метод, а объект, вызывающий метод с требуемой сигнатурой. Тогда вся требуемая методу информация может содержаться в полях объекта. Этот способ передачи данных при работе с потоками, как мы знаем, является одним из наиболее надежных.
Ну и наконец, можно передать потоку анонимный метод. Преимущество этого способа в том, что в этом случае анонимный метод может вызывать метод с произвольной сигнатурой. Напомню, что в "Потоки и параллельные вычисления" был приведен пример вызова анонимного метода, приводящий к некорректной работе приложения. Так что при работе с анонимными методами следует быть осторожным.
При работе с объектами класса Thread, метод, который должен выполнять поток, передавался конструктору объектов класса Thread. После этого для запуска созданного потока вызывался метод Start, которым обладают объекты класса Thread. При работе с пулом потоков приходится идти другим путем, поскольку нет конструкторов класса ThreadPool, нет создаваемых объектов этого класса. Вот как происходит связывание определяющего задачу метода с потоком из пула. У класса ThreadPool есть статический метод QueueUserWorkItem, позволяющий поставить задачу в очередь на выполнение. Метод является функцией, возвращающей true, если задача успешно поставлена в очередь, и false - в противном случае. Метод перегружен. Ему можно передавать один или два параметра. Первым параметром является объект класса WaitCallback, представляющий передаваемый потоку метод. Вторым параметром, если он задан, является объект универсального класса object. Первый параметр несет информацию о методе, передаваемом потоку, второй - содержит информацию, передаваемую методу, являясь фактическим параметром метода.
Давайте перейдем к примеру, в котором продемонстрируем разные способы передачи методов пулу потоков:
static void Main(string[] args) { //Потоку передается метод со стандартной сигнатурой void(object) //Информация через параметр методу не передается //ThreadPool.QueueUserWorkItem(new WaitCallback(WorkerOne)) ; ThreadPool.QueueUserWorkItem(WorkerOne);
В этом фрагменте процедуры Main пулу потоков передается метод WorkerOne. В закомментированной строке демонстрируется явный способ конструирования объекта, заданного делегатом WaitCallback. Проще указать имя метода, выдержав требования к сигнатуре.
Приведу текст метода WorkerOne:
static void WorkerOne(object info) { Console.WriteLine("Я работник первый!"); }
Как видите, метод соответствует требуемой сигнатуре void (object). Заметьте также, что метод не использует информацию, передаваемую параметром info.
Добавим в Main следующий фрагмент кода:
//Потоку передается метод со стандартной сигнатурой void(object) //методу передается информация через параметр object inf = new Info("Феликс", 50); ThreadPool.QueueUserWorkItem(new WaitCallback(WorkerTwo), inf);
Метод WorkerTwo, передаваемый пулу потоков, будет использовать передаваемую ему информацию. Вот текст этого метода:
static void WorkerTwo(object info) { Info inf = (Info)info; Console.WriteLine("Я работник второй!"); Console.WriteLine("Меня зовут " + inf.Name); Console.WriteLine("Мне" + inf.Age.ToString() ); }
Переданный методу параметр info приводится к типу Info и затем с ним можно работать. Тип Info задается следующей структурой:
struct Info { string name; int age; public Info(string name, int age) { this.name = name; this.age = age; } public string Name { get { return name; } } public int Age { get { return age; } } }
Следующий фрагмент кода, добавляемый в процедуру Main, демонстрирует передачу потоку анонимного метода:
//Потоку передается анонимный метод с произвольной сигнатурой //Информация методу передается в момент вызова //ThreadPool.QueueUserWorkItem //(new WaitCallback((name) => { WorkerThree("Дмитрий"); })); ThreadPool.QueueUserWorkItem((name) => { WorkerThree("Дмитрий"); });
Заметьте, и в случае анонимного метода явно конструировать объект WaitCallback не требуется. Приведу текст метода WorkerThree, вызываемого анонимным методом:
static void WorkerThree(string name) { Console.WriteLine("Я работник третий!"); Console.WriteLine("Меня зовут " + name); }
Добавим еще один фрагмент в процедуру Main, демонстрирующий передачу пулу потоков метода вместе с объектом, вызывающим этот метод:
//Потоку передается экземплярный метод со стандартной сигнатурой void(object) //Информация методу содержится в полях объекта, вызывающего метод Simple sim = new Simple("Олег", 33, "программист"); ThreadPool.QueueUserWorkItem(new WaitCallback(sim.About)); object inf_add = "Прекрасный работник"; ThreadPool.QueueUserWorkItem(new WaitCallback(sim.About), inf_add);
Здесь вначале создается объект sim класса Simple. Затем пулу потоков передается этот объект, вызывающий метод About из класса Simple. Операция выполняется дважды, во втором случае помимо информации, содержащейся в самом вызывающем объекте sim, метод может использовать информацию, передаваемую через параметр inf_add. Рассмотрим текст класса Simple:
class Simple { string name; int age; string profession; public Simple(string name, int age, string profession) { this.name = name; this.age = age; this.profession = profession; } public void About(object inf) { Console.WriteLine("Новый работник. Мое имя - {0}, " + " возраст - {1} профессия - {2}", name, age, profession); if (inf != null) Console.WriteLine(inf.ToString()); } }
В результате приведенных фрагментов кода в очередь к пулу потоков поставлены пять наших задач. Осталось синхронизировать работу потоков из пула потоков и основного потока, выполняющего процедуру Main. Рассмотрим простейший способ синхронизации, когда основной поток засыпает на некоторое время, давая шанс на выполнение потокам в фоновом режиме. Вот как выглядит заключительный фрагмент процедуры Main, работающий после того, как задачи поставлены в очередь к пулу потоков:
//Основной поток засыпает //В это время потоки пула выполняются Thread.Sleep(100); //Основной поток проснулся и продолжил работу Console.WriteLine("Я управляющий!");
Осталось посмотреть на результаты работы потоков:
Можно видеть, что все задачи успешно выполнились. Но, обратите внимание, потоки работали параллельно и для вывода своих результатов использовали общий ресурс - Console.
По этой причине результаты разных потоков идут вперемешку и не в том порядке, как задачи ставились в очередь. Для синхронизации, поддерживающей строгий порядок выполнения, требуются определенные усилия. Пока же наш пример явно демонстрирует параллелизм вычислений задач, выполняемых пулом потоков. В этом и была цель нашего примера.
Пример. Обсуждение
Достоинства и недостатки последнего примера заслуживают более серьезного обсуждения. Несомненным достоинством является относительная простота и возможность вариации при связывании задач с потоками.
О недостатках стоит поговорить подробнее. Первый очевидный недостаток состоит в том, что результаты работы отдельных задач при выводе на консоль перемешиваются, создавая эффект "блюда спагетти". Этот недостаток целиком лежит на нашей совести. Мы допустили типичную ошибку при работе с потоками, заставив потоки участвовать в Data Race (Condition) - гонке данных (условий) в борьбе за единый разделяемый ресурс - консоль вывода. Как необходимо было организовать процесс получения результатов? Задаче следует передавать входные данные, а она должна формировать результаты - выходные данные, не нарушая важный принцип отделения бизнес-логики от интерфейса. Вывод на консоль должен осуществлять интерфейсный метод, в данном случае основной поток, который должен получить результаты решения задач, выполненных в дочерних потоках, и организовать выдачу этих результатов в нужном порядке. Если следовать правилам технологии, то данный недостаток нашего примера легко устраним.
Второй, более серьезный недостаток не преодолевается столь просто, поскольку он характерен для класса ThreadPool. Возможности этого класса не позволяют нужным образом организовать синхронизацию работы основного потока и задач, исполняемых в пуле потоков. В нашем примере для того, чтобы запустить на выполнение потоки пула, основной поток засыпал на некоторое время. Это прием годится для примеров, таких как наш, но непригоден для серьезных приложений, где непонятно, какое время понадобится для выполнения задач, решаемых в пуле потоков. Плохо и то, что потоки в пуле обезличены, мы не знаем "кто сшил костюм", мы не можем выполнить операцию, аналогичную Join, чтобы гарантировано дождаться завершения определенной задачи.