Читаем Полное руководство. С# 4.0 полностью

Применяя TPL, параллелизм в программу можно ввести двумя основными способа ми. Первый из них называется параллелизмом данных. При таком подходе одна опера ция над совокупностью данных разбивается на два параллельно выполняемых потока или больше, в каждом из которых обрабатывается часть данных. Так, если изменяется каждый элемент массива, то, применяя параллелизм данных, можно организовать па раллельную обработку разных областей массива в двух или больше потоках. Нетрудно догадаться, что такие параллельно выполняющиеся действия могут привести к значи тельному ускорению обработки данных по сравнению с последовательным подходом. Несмотря на то что параллелизм данных был всегда возможен и с помощью класса Thread, построение масштабируемых решений средствами этого класса требовало не мало усилий и времени. Это положение изменилось с появлением библиотеки TPL, с помощью которой масштабируемый параллелизм данных без особого труда вводится в программу. Второй способ ввода параллелизм называется параллелизмом задач. При таком под ходе две операции или больше выполняются параллельно. Следовательно, паралле лизм задач представляет собой разновидность параллелизма, который достигался в прошлом средствами класса Thread. А к преимуществам, которые сулит применение TPL, относится простота применения и возможность автоматически масштабировать исполнение кода на несколько процессоров. Класс Task В основу TPL положен класс Task. Элементарная единица исполнения инкапсу лируется в TPL средствами класса Task, а не Thread. Класс Task отличается от класса Thread тем, что он является абстракцией, представляющей асинхронную операцию. А в классе Thread инкапсулируется поток исполнения. Разумеется, на системном уров не поток по-прежнему остается элементарной единицей исполнения, которую можно планировать средствами операционной системы. Но соответствие экземпляра объекта класса Task и потока исполнения не обязательно оказывается взаимно-однозначным. Кроме того, исполнением задач управляет планировщик задач, который работает с пу дом потоков. Это, например, означает, что несколько задач могут разделять один и тот же поток. Класс Task (и вся остальная библиотека TPL) определены в пространстве имен System.Threading.Tasks. Создание задачи Создать новую задачу в виде объекта класса Task и начать ее исполнение можно самыми разными способами. Для начала создадим объект типа Task с помощью кон структора и запустим его, вызвав метод Start. Для этой цели в классе Task опреде лено несколько конструкторов. Ниже приведен тот конструктор, которым мы собира емся воспользоваться: public Task(Action действие) где действие обозначает точку входа в код, представляющий задачу, тогда как Action — делегат, определенный в пространстве имен System. Форма делегата Action, которой мы собираемся воспользоваться, выглядит следующим образом. public delegate void Action Таким образом, точкой входа должен служить метод, не принимающий никаких параметров и не возвращающий никаких значений. (Как будет показано далее, делега ту Action можно также передать аргумент.) Как только задача будет создана, ее можно запустить на исполнение, вызвав метод Start. Ниже приведена одна из его форм. public void Start После вызова метода Start планировщик задач запланирует исполнение задачи. В приведенной ниже программе все изложенное выше демонстрируется на прак тике. В этой программе отдельная задача создается на основе метода MyTask. После того как начнет выполняться метод Main, задача фактически создается и запускается на исполнение. Оба метода MyTask и Main выполняются параллельно. // Создать и запустить задачу на исполнение. using System; using System.Threading; using System.Threading.Tasks; class DemoTask { // Метод выполняемый в качестве задачи. static void MyTask { Console.WriteLine("MyTask запущен"); for(int count = 0; count < 10; count++) { Thread.Sleep(500); Console.WriteLine ("В методе MyTask, подсчет равен " + count); } Console.WriteLine("MyTask завершен"); } static void Main { Console.WriteLine("Основной поток запущен."); // Сконструировать объект задачи. Task tsk = new Task(MyTask); // Запустить задачу на исполнение. tsk.Start; // метод Main активным до завершения метода MyTask. for(int i = 0; i < 60; i++) { Console.Write("."); Thread.Sleep(100); } Console.WriteLine("Основной поток завершен."); } } Ниже приведен результат выполнения этой программы. (У вас он может несколько отличаться в зависимости от загрузки задач, операционной системы и прочих факторов.) Основной поток запущен. .MyTask запущен .....В методе MyTask, подсчет равен 0 .....В методе MyTask, подсчет равен 1 .....В методе MyTask, подсчет равен 2 .....В методе MyTask, подсчет равен 3 .....В методе MyTask, подсчет равен 4 .....В методе MyTask, подсчет равен 5 .....В методе MyTask, подсчет равен 6 .....В методе MyTask, подсчет равен 7 .....В методе MyTask, подсчет равен 8 .....В методе MyTask, подсчет равен 9 MyTask завершен .........Основной поток завершен. Следует иметь в виду, что по умолчанию задача исполняется в фоновом потоке. Следовательно, при завершении создающего потока завершается и сама задача. Имен но поэтому в рассматриваемой здесь программе метод Thread.Sleep использован для сохранения активным основного потока до тех пор, пока не завершится выполне ние метода MyTask. Как и следовало ожидать, организовать ожидание завершения задачи можно и более совершенными способами, что и будет показано далее. В приведенном выше примере программы задача, предназначавшаяся для парал лельного исполнения, обозначалась в виде статического метода. Но такое требование к задаче не является обязательным. Например, в приведенной ниже программе, которая является переработанным вариантом предыдущей, метод MyTask, выполняющий роль задачи, инкапсулирован внутри класса. // Использовать метод экземпляра в качестве задачи. using System; using System.Threading; using System.Threading.Tasks; class MyClass { // Метод выполняемый в качестве задачи. public void MyTask { Console.WriteLine("MyTask запущен"); for (int count = 0; count < 10; count++) { Thread.Sleep(500); Console.WriteLine("В методе MyTask, подсчет равен " + count); } Console.WriteLine("MyTask завершен "); } } class DemoTask { static void Main { Console.WriteLine("Основной поток запущен."); // Сконструировать объект типа MyClass. MyClass mc = new MyClass; // Сконструировать объект задачи для метода me.MyTask. Task tsk = new Task(mc.MyTask); // Запустить задачу на исполнение. tsk.Start; // Сохранить метод Main активным до завершения метода MyTask. for(int i = 0; i < 60; i++) { Console.Write("."); Thread.Sleep(100); } Console.WriteLine("Основной поток завершен."); } } Результат выполнения этой программы получается таким же, как и прежде. Един ственное отличие состоит в том, что метод MyTask вызывается теперь для экземпляра объекта класса MyClass. В отношении задач необходимо также иметь в виду следующее: после того, как за дача завершена, она не может быть перезапущена. Следовательно, иного способа по вторного запуска задачи на исполнение, кроме создания ее снова, не существует. Применение идентификатора задачи В отличие от класса Thread; в классе Task отсутствует свойство Name для хранения имени задачи. Но вместо этого в нем имеется свойство Id для хранения идентификато ра задачи, по которому можно распознавать задачи. Свойство Id доступно только для чтения и относится к типу int. Оно объявляется следующим образом. public int Id { get; } Каждая задача получает идентификатор, когда она создается. Значения идентифи каторов уникальны, но не упорядочены. Поэтому один идентификатор задачи может появиться перед другим, хотя он может и не иметь меньшее значение. Идентификатор исполняемой в настоящий момент задачи можно выявить с помо щью свойства CurrentId. Это свойство доступно только для чтения, относится к типу static и объявляется следующим образом. public static Nullable CurrentID { get; } Оно возвращает исполняемую в настоящий момент задачу или же пустое значение, если вызывающий код не является задачей. В приведенном ниже примере программы создаются две задачи и показывается, какая из них исполняется. // Продемонстрировать применение свойств Id и CurrentId. using System; using System.Threading; using System.Threading.Tasks; class DemoTask { // Метод, исполняемый как задача. static void MyTask { Console.WriteLine("MyTask №" + Task.CurrentId + " запущен"); for(int count = 0; count < 10; count++) { Thread.Sleep(500); Console.WriteLine("В методе MyTask #" + Task.CurrentId + ", подсчет равен " + count ); } Console.WriteLine("MyTask №" + Task.CurrentId + " завершен"); } static void Main { Console.WriteLine("Основной поток запущен."); // Сконструировать объекты двух задач. Task tsk = new Task(MyTask); Task tsk2 = new Task(MyTask); // Запустить задачи на исполнение, tsk.Start; tsk2.Start; Console.WriteLine("Идентификатор задачи tsk: " + tsk.Id); Console.WriteLine("Идентификатор задачи tsk2: " + tsk2.Id); // Сохранить метод Main активным до завершения остальных задач. for(int i = 0; i < 60; i++) { Console.Write ("."); Thread.Sleep(100); } Console.WriteLine("Основной поток завершен."); } Выполнение этой программы приводит к следующему результату. Основной поток запущен Идентификатор задачи tsk: 1 Идентификатор задачи tsk2: 2 .MyTask №1 запущен MyTask №2 запущен .....В методе MyTask №1, подсчет равен 0 В методе MyTask №2, подсчет равен 0 .....В методе MyTask №2, подсчет равен 1 В методе MyTask №1, подсчет равен 1 .....В методе MyTask №1, подсчет равен 2 В методе MyTask №2, подсчет равен 2 .....В методе MyTask №2, подсчет равен 3 В методе MyTask №1, подсчет равен 3 .....В методе MyTask №1, подсчет равен 4 В методе MyTask №2, подсчет равен 4 .....В методе MyTask №1, подсчет равен 5 В методе MyTask №2, подсчет равен 5 .....В методе MyTask №2, подсчет равен 6 В методе MyTask №1, подсчет равен 6 .....В методе MyTask №2, подсчет равен 7 В методе MyTask №1, подсчет равен 7 .....В методе MyTask №1, подсчет равен 8 В методе MyTask №2, подсчет равен 8 .....В методе MyTask №1, подсчет равен 9 MyTask №1 завершен В методе MyTask №2, подсчет равен 9 MyTask №2 завершен .........Основной поток завершен. Применение методов ожидания В приведенных выше примерах основной поток исполнения, а по существу, ме тод Main, завершался потому, что такой результат гарантировали вызовы мето да Thread.Sleep. Но подобный подход нельзя считать удовлетворительным. Организовать ожидание завершения задач можно и более совершенным способом, применяя методы ожидания, специально предоставляемые в классе Task. Самым про стым из них считается метод Wait, приостанавливающий исполнение вызывающего потока до тех пор, пока не завершится вызываемая задача. Ниже приведена простей шая форма объявления этого метода. public void Wait При выполнении этого метода могут быть сгенерированы два исключения. Первым из них является исключение ObjectDisposedException. Оно генерируется в том случае, если задача освобождена посредством вызова метода Dispose. А второе ис ключение, AggregateException, генерируется в том случае, если задача сама генери рует исключение или же отменяется. Как правило, отслеживается и обрабатывается именно это исключение. В связи с тем что задача может сгенерировать не одно ис ключение, если, например, у нее имеются порожденные задачи, все подобные исклю чения собираются в единое исключение типа AggregateException. Для того чтобы выяснить, что же произошло на самом деле, достаточно проанализировать внутренние исключения, связанные с этим совокупным исключением. А до тех пор в приведенных далее примерах любые исключения, генерируемые задачами, будут обрабатываться во время выполнения. Ниже приведен вариант предыдущей программы, измененный с целью продемон стрировать применение метода Wait на практике. Этот метод используется внутри метода Main, чтобы приостановить его выполнение до тех пор, пока не завершатся обе задачи tsk и tsk2. // Применить метод Wait. using System; using System.Threading; using System.Threading.Tasks; class DemoTask { // Метод, исполняемый как задача. static void MyTask { Console.WriteLine("MyTask №" + Task.CurrentId + " запущен"); for(int count = 0; count < 10; count++) { Thread.Sleep(500); Console.WriteLine("В методе MyTask #" + Task.CurrentId + ", подсчет равен " + count ); } Console.WriteLine("MyTask №" + Task.CurrentId + " завершен"); } static void Main { Console.WriteLine("Основной поток запущен."); // Сконструировать объекты двух задач. Task tsk = new Task(MyTask); Task tsk2 = new Task(MyTask); // Запустить задачи на исполнение. tsk.Start; tsk2.Start; Console.WriteLine("Идентификатор задачи tsk: " + tsk.Id); Console.WriteLine("Идентификатор задачи tsk2: " + tsk2.Id); // Приостановить выполнение метода Main до тех пор, // пока не завершатся обе задачи tsk и tsk2 tsk.Wait; tsk2.Wait; Console.WriteLine("Основной поток завершен."); } } При выполнении этой программы получается следующий результат. Основной поток запущен Идентификатор задачи tsk: 1 Идентификатор задачи tsk2: 2 MyTask №1 запущен MyTask №2 запущен В методе MyTask №1, подсчет равен 0 В методе MyTask №2, подсчет равен 0 В методе MyTask №1, подсчет равен 1 В методе MyTask №2, подсчет равен 1 В методе MyTask №1, подсчет равен 2 В методе MyTask №2, подсчет равен 2 В методе MyTask №1, подсчет равен 3 В методе MyTask №2, подсчет равен 3 В методе MyTask №1, подсчет равен 4 В методе MyTask №2, подсчет равен 4 В методе MyTask №1, подсчет равен 5 В методе MyTask №2, подсчет равен 5 В методе MyTask №1, подсчет равен 6 В методе MyTask №2, подсчет равен 6 В методе MyTask №1, подсчет равен 7 В методе MyTask №2, подсчет равен 7 В методе MyTask №1, подсчет равен 8 В методе MyTask №2, подсчет равен 8 В методе MyTask №1, подсчет равен 9 MyTask №1 завершен В методе MyTask №2, подсчет равен 9 MyTask №2 завершен Основной поток завершен. Как следует из приведенного выше результата, выполнение метода Main приоста навливается до тех пор, пока не завершатся обе задачи tsk и tsk2. Следует, однако, иметь в виду, что в рассматриваемой здесь программе последовательность завершения задач tsk и tsk2 не имеет особого значения для вызовов метода Wait. Так, если первой за вершается задача tsk2, то в вызове метода tsk.Wait будет по-прежнему ожидаться завершение задачи tsk. В таком случае вызов метода tsk2.Wait приведет к выполне нию и немедленному возврату из него, поскольку задача tsk2 уже завершена. В данном случае оказывается достаточно двух вызовов метода Wait, но того же результата можно добиться и более простым способом, воспользовавшись методом WaitAll. Этот метод организует ожидание завершения группы задач. Возврата из него не произойдет до тех пор, пока не завершатся все задачи. Ниже приведена про стейшая форма объявления этого метода. public static void WaitAll(params Task[] tasks) Задачи, завершения которых требуется ожидать, передаются с помощью пара метра в виде массива tasks. А поскольку этот параметр относится к типу params, то данному методу можно отдельно передать массив объектов типа Task или спи сок задач. При этом могут быть сгенерированы различные исключения, включая и AggregateException. Для того чтобы посмотреть, как метод WaitAll действует на практике, замените в приведенной выше программе следующую последовательность вызовов. tsk.Wait; tsk2.Wait; на Task.WaitAll(tsk, tsk2); Программа будет работать точно так же, но логика ее выполнения станет более понятной. Организуя ожидание завершения нескольких задач, следует быть особенно внима тельным, чтобы избежать взаимоблокировок. Так, если две задачи ожидают заверше ния друг друга, то вызов метода WaitAll вообще не приведет к возврату из него. Разумеется, условия для взаимоблокировок возникают в результате ошибок програм мирования, которых следует избегать. Следовательно, если вызов метода WaitAll не приводит к возврату из него, то следует внимательно проанализировать, могут ли две задачи или больше взаимно блокироваться. (Вызов метода Wait, который не приводит к возврату из него, также может стать причиной взаимоблокировок.) Иногда требуется организовать ожидание до тех пор, пока не завершится любая из группы задач. Для этой цели служит метод WaitAny. Ниже приведена простейшая форма его объявления. public static int WaitAny(params Task[] tasks) Задачи, завершения которых требуется ожидать, передаются с помощью параме тра в виде массива tasks объектов типа Task или отдельного списка аргументов типа Task. Этот метод возвращает индекс задачи, которая завершается первой. При этом могут быть сгенерированы различные исключения. Попробуйте применить метод WaitAny на практике, подставив в предыдущей программе следующий вызов. Task.WaitAny(tsk, tsk2); Теперь, выполнение метода Main возобновится, а программа завершится, как только завершится одна из двух задач. Помимо рассматривавшихся здесь форм методов Wait, WaitAll и WaitAny, имеются и другие их варианты, в которых можно указывать период про стоя или отслеживать признак отмены. (Подробнее об отмене задач речь пойдет да лее в этой главе.) Вызов метода Dispose В классе Task реализуется интерфейс IDisposable, в котором определяется метод Dispose. Ниже приведена форма его объявления. public void Dispose Метод Dispose реализуется в классе Task, освобождая ресурсы, используемые этим классом. Как правило, ресурсы, связанные с классом Task, освобождаются авто матически во время "сборки мусора" (или по завершении программы). Но если эти ресурсы требуется освободить еще раньше, то для этой цели служит метод Dispose. Это особенно важно в тех программах, где создается большое число задач, оставляемых на произвол судьбы. Следует, однако, иметь в виду, что метод Dispose можно вызывать для отдель ной задачи только после ее завершения. Следовательно, для выяснения факта завер шения отдельной задачи, прежде чем вызывать метод Dispose, потребуется неко торый механизм, например, вызов метода Wait. Именно поэтому так важно было рассмотреть метод Wait, перед тем как обсуждать метод Dispose. Ели же по пытаться вызвать Dispose для все еще активной задачи, то будет сгенерировано ис ключение InvalidOperationException. Во всех примерах, приведенных в этой главе, создаются довольно короткие задачи, которые фазу же завершаются, и поэтому применение метода Dispose в этих приме рах не дает никаких преимуществ. (Именно по этой причине вызывать метод Dispose в приведенных выше программах не было никакой необходимости. Ведь все они заверша лись, как только завершалась задача, что в конечном итоге приводило к освобождению от остальных задач.) Но в целях демонстрации возможностей данного метода и во избежа ние каких-либо недоразумений метод Dispose будет вызываться явным образом при непосредственном обращении с экземплярами объектов типа Task во всех последующих примерах программ. Если вы обнаружите отсутствие вызовов метода Dispose в ис ходном коде, полученном из других источников, то не удивляйтесь этому. Опять же, если программа завершается, как только завершится задача, то вызывать метод Dispose нет никакого смысла — разве что в целях демонстрации его применения. Применение класса TaskFactory для запуска задачи Приведенные выше примеры программы были составлены не так эффективно, как следовало бы, поскольку задачу можно создать и сразу же начать ее исполнение, вы звав метод StartNew, определенный в классе TaskFactory. В классе TaskFactory предоставляются различные методы, упрощающие создание задач и управление ими. По умолчанию объект класса TaskFactory может быть получен из свойства Factory, доступного только для чтения в классе Task. Используя это свойство, можно вызвать любые методы класса TaskFactory. Метод StartNew существует во множестве форм. Ниже приведена самая простая форма его объявления: public Task StartNew(Action action) где action — точка входа в исполняемую задачу. Сначала в методе StartNew авто матически создается экземпляр объекта типа Task для действия, определяемого па раметром action, а затем планируется запуск задачи на исполнение. Следовательно, необходимость в вызове метода Start теперь отпадает. Например, следующий вызов метода StartNew в рассматривавшихся ранее про граммах приведет к созданию и запуску задачи tsk одним действием. Task tsk = Task.Factory.StartNew(MyTask); После этого оператора сразу же начнет выполняться метод MyTask. Метод StartNew оказывается более эффективным в тех случаях, когда задача соз дается и сразу же запускается на исполнение. Поэтому именно такой подход и при меняется в последующих примерах программ. Применение лямбда-выражения в качестве задачи Кроме использования обычного метода в качестве задачи, существует и другой, бо лее рациональный подход: указать лямбда-выражение как отдельно решаемую задачу. Напомним, что лямбда-выражения являются особой формой анонимных функций. По этому они могут исполняться как отдельные задачи. Лямбда-выражения оказываются особенно полезными в тех случаях, когда единственным назначением метода является решение одноразовой задачи. Лямбда-выражения могут составлять отдельную задачу иди же вызывать другие методы. Так или иначе, применение лямбда-выражения в каче стве задачи может стать привлекательной альтернативой именованному методу. В приведенном ниже примере программы демонстрируется применение лямбда- выражения в качестве задачи. В этой программе код метода MyTask из предыдущих примеров программ преобразуется в лямбда-выражение. // Применить лямбда-выражение в качестве задачи. using System; using System.Threading; using System.Threading.Tasks; class DemoLambdaTask { static void Main { Console.WriteLine("Основной поток запущен."); // Далее лямбда-выражение используется для определения задачи. Task tsk = Task.Factory.StartNew( => { Console.WriteLine("Задача запущена"); for (int count = 0; count < 10; count++) { Thread.Sleep(500); Console.WriteLine("Подсчет в задаче равен " + count ); } Console.WriteLine("Задача завершена"); } ); // Ожидать завершения задачи tsk. tsk.Wait; // Освободить задачу tsk. tsk.Dispose; Console.WriteLine("Основной поток завершен."); } } Ниже приведен результат выполнения этой программы. Основной поток запущен. Задача запущена Подсчет в задаче равен 0 Подсчет в задаче равен 1 Подсчет в задаче равен 2 Подсчет в задаче равен 3 Подсчет в задаче равен 4 Подсчет в задаче равен 5 Подсчет в задаче равен 6 Подсчет в задаче равен 7 Подсчет в задаче равен 8 Подсчет в задаче равен 9 Задача завершена Основной поток завершен. Помимо применения лямбда-выражения для описания задачи, обратите также внимание в данной программе на то, что вызов метода tsk.Dispose не делается до тех пор, пока не произойдет возврат из метода tsk.Wait. Как пояснялось в пред ыдущем разделе, метод Dispose можно вызывать только по завершении задачи. Для того чтобы убедиться в этом, попробуйте поставить вызов метода tsk.Dispose в рассматриваемой здесь программе перед вызовом метода tsk.Wait. Вы сразу же заметите, что это приведет к исключительной ситуации. Создание продолжения задачи Одной из новаторских и очень удобных особенностей библиотеки TPL является воз можность создавать продолжение задачи. Продолжение — это одна задача, которая ав томатически начинается после завершения другой задачи. Создать продолжение мож но, в частности, с помощью метода ContinueWith, определенного в классе Task. Ниже приведена простейшая форма его объявления: public Task ContinueWith(Action действиепродолжения) где действиепродолжения обозначает задачу, которая будет запущена на исполне ние по завершении вызывающей задачи. У делегата Action имеется единственный па раметр типа Task. Следовательно, вариант делегата Action, применяемого в данном методе, выглядит следующим образом. public delegate void Action(T obj) В данном случае обобщенный параметр T обозначает класс Task. Продолжение задачи демонстрируется на примере следующей программы. // Продемонстрировать продолжение задачи. using System; using System.Threading; using System.Threading.Tasks; class ContinuationDemo { // Метод, исполняемый как задача. static void MyTask { Console.WriteLine("MyTask запущен"); for(int count = 0; count < 5; count++) { Thread.Sleep(500); Console.WriteLine("В методе MyTask подсчет равен " + count ); } Console.WriteLine("MyTask завершен"); } // Метод, исполняемый как продолжение задачи. static void ContTask(Task t) { Console.WriteLine("Продолжение запущено"); for(int count = 0; count < 5; count++) { Thread.Sleep(500); Console.WriteLine("В продолжении подсчет равен " + count ); } Console.WriteLine("Продолжение завершено"); } static void Main { Console.WriteLine("Основной поток запущен."); // Сконструировать объект первой задачи. Task tsk = new Task(MyTask); // А теперь создать продолжение задачи. Task taskCont = tsk.ContinueWith(ContTask); // Начать последовательность задач. tsk.Start; // Ожидать завершения продолжения. taskCont.Wait; tsk.Dispose; taskCont.Dispose; Console.WriteLine("Основной поток завершен."); } } Ниже приведен результата выполнения данной программы. Основной поток запущен. MyTask запущен В методе MyTask подсчет равен 0 В методе MyTask подсчет равен 1 В методе MyTask подсчет равен 2 В методе MyTask подсчет равен 3 В методе MyTask подсчет равен 4 MyTask завершен Продолжение запущено В продолжении подсчет равен 0 В продолжении подсчет равен 1 В продолжении подсчет равен 2 В продолжении подсчет равен 3 В продолжении подсчет равен 4 Продолжение завершено Основной поток завершен. Как следует из приведенного выше результата, вторая задача не начинается до тех пор, пока не завершится первая. Обратите также внимание на то, что в методе Main пришлось ожидать окончания только продолжения задачи. Дело в том, что метод MyTask как задача завершается еще до начала метода ContTask как продолжения задачи. Следовательно, ожидать завершения метода MyTask нет никакой надобно сти, хотя если и организовать такое ожидание, то в этом будет ничего плохого. Любопытно, что в качестве продолжения задачи нередко применяется лямбда- выражение. Для примера ниже приведен еще один способ организации продолжения задачи из предыдущего примера программы. // В данном случае в качестве продолжения задачи применяется лямбда-выражение. Task taskCont = tsk.ContinueWith((first) => { Console.WriteLine("Продолжение запущено"); for(int count = 0; count < 5; count++) { Thread.Sleep(500); Console.WriteLine("В продолжении подсчет равен " + count ); } Console.WriteLine("Продолжение завершено"); } }; В этом фрагменте кода параметр first принимает предыдущую задачу (в данном случае — tsk). Помимо метода ContinueWith, в классе Task предоставляются и другие методы, поддерживающие продолжение задачи, обеспечиваемое классом TaskFactory. К их чис лу относятся различные формы методов ContinueWhenAny и ContinueWhenAll, которые продолжают задачу, если завершится любая или все указанные задачи соот ветственно. Возврат значения из задачи Задача может возвращать значение. Это очень удобно по двум причинам. Во-первых, это означает, что с помощью задачи можно вычислить некоторый резуль тат. Подобным образом поддерживаются параллельные вычисления. И во-вторых, вы зывающий процесс окажется блокированным до тех пор, пока не будет получен ре зультат. Это означает, что для организации ожидания результата не требуется никакой особой синхронизации. Для того чтобы возвратить результат из задачи, достаточно создать эту задачу, ис пользуя обобщенную форму Task класса Task. Ниже приведены два кон структора этой формы класса Task: public Task(Func функция) public Task(Func функция, Object состояние) где функция обозначает выполняемый делегат. Обратите внимание на то, что он дол жен быть типа Func, а не Action. Тип Func используется именно в тех случаях, когда задача возвращает результат. В первом конструкторе создается задача без аргументов, а во втором конструкторе — задача, принимающая аргумент типа Object, передавае мый как состояние. Имеются также другие конструкторы данного класса. Как и следовало ожидать, имеются также другие варианты метода StartNew, доступные в обобщенной форме класса TaskFactory и поддерживающие возврат результата из задачи. Ниже приведены те варианты данного метода, которые применяются параллельно с только что рассмотренными конструкторами класса Task. public Task StartNew(Func функция) public Task StartNew(Func функция, Object состояние) В любом случае значение, возвращаемое задачей, подучается из свойства Result в классе Task, которое определяется следующим образом. public TResult Result { get; internal set; } Аксессор set является внутренним для данного свойства, и поэтому оно оказывает ся доступным во внешнем коде, по существу, только для чтения. Следовательно, задача получения результата блокирует вызывающий код до тех пор, пока результат не будет вычислен. В приведенном ниже примере программы демонстрируется возврат задачей значе ний. В этой программе создаются два метода. Первый из них, MyTask, не принимает параметров, а просто возвращает логическое значение true типа bool. Второй метод, SumIt, принимает единственный параметр, который приводится к типу int, и воз вращает сумму из значения, передаваемого в качестве этого параметра. // Возвратить значение из задачи. using System; using System.Threading; using System.Threading.Tasks; class DemoTask { // Простейший метод, возвращающий результат и не принимающий аргументов. static bool MyTask { return true; } // Этот метод возвращает сумму из положительного целого значения, // которое ему передается в качестве единственного параметра static int Sumlt(object v) { int x = (int) v; int sum = 0; for(; x > 0; x--) sum += x; return sum; } static void Main { Console.WriteLine("Основной поток запущен."); // Сконструировать объект первой задачи. Task tsk = Task.Factory.StartNew(MyTask); Console.WriteLine("Результат после выполнения задачи MyTask: " + tsk.Result); // Сконструировать объект второй задачи. Task tsk2 = Task.Factory.StartNew(Sumlt, 3); Console.WriteLine("Результат после выполнения задачи Sumlt: " + tsk2.Result); tsk.Dispose; tsk2.Dispose; Console.WriteLine("Основной поток завершен."); } } Выполнение этой программы приводит к следующему результату. Основной поток запущен. Результат после выполнения задачи MyTask: True Результат после выполнения Sumlt: 6 Основной поток завершен. Помимо упомянутых выше форм класса Task и метода StartNew, имеются также другие формы. Они позволяют указывать другие дополнительные параметры. Отмена задачи и обработка исключения AggregateException В версии 4.0 среды .NET Framework внедрена новая подсистема, обеспечивающая структурированный, хотя и очень удобный способ отмены задачи. Эта новая подсисте ма основывается на понятии признака отмены. Признаки отмены поддерживаются в классе Task, среди прочего, с помощью фабричного метода StartNew. ПРИМЕЧАНИЕ Новую подсистему отмены можно применять и для отмены потоков, рассматривавшихся в предыдущей главе, но она полностью интегрирована в TPL и PLINQ. Именно поэтому эта подсистема рассматривается в этой главе. Отмена задачи, как правило, выполняется следующим образом. Сначала полу чается признак отмены из источника признаков отмены. Затем этот признак пере дается задаче, после чего она должна контролировать его на предмет получения за проса на отмену. (Этот запрос может поступить только из источника признаков отмены.) Если получен запрос на отмену, задача должна завершиться. В одних слу чаях этого оказывается достаточно для простого прекращения задачи без каких- либо дополнительных действий, а в других — из задачи должен быть вызван метод ThrowIfCancellationRequested для признака отмены. Благодаря этому в отме няющем коде становится известно, что задача отменена. А теперь рассмотрим процесс отмены задачи более подробно. Признак отмены является экземпляром объекта типа CancellationToken, т.е. структуры, определенной в пространстве имен System.Threading. В струк туре CancellationToken определено несколько свойств и методов, но мы вос пользуемся двумя из них. Во-первых, это доступное только для чтения свойство IsCancellationRequested, которое объявляется следующим образом. public bool IsCancellationRequested { get; } Оно возвращает логическое значение true, если отмена задачи была запрошена для вызывающего признака, а иначе — логическое значение false. И во-вторых, это метод ThrowIfCancellationRequested, который объявляется следующим образом. public void ThrowIfCancellationRequested Если признак отмены, для которого вызывается этот метод, получил запрос на от мену, то в данном методе генерируется исключение OperationCanceledException. В противном случае никаких действий не выполняется. В отменяющем коде можно организовать отслеживание упомянутого исключения с целью убедиться в том, что отмена задачи действительно произошла. Как правило, с этой целью сначала пере хватывается исключение AggregateException, а затем его внутреннее исключение анализируется с помощью свойства InnerException или InnerExceptions. (Свой ство InnerExceptions представляет собой коллекцию исключений. Подробнее о кол лекциях речь пойдет в главе 25.) Признак отмены получается из источника признаков отмены, который пред ставляет собой объект класса CancellationTokenSource, определенного в про странстве имен System. Threading. Для того чтобы получить данный признак, нуж но создать сначала экземпляр объекта типа CancellationTokenSource. (С этой целью можно воспользоваться вызываемым по умолчанию конструктором класса CancellationTokenSource.) Признак отмены, связанный с данным источником, ока зывается доступным через используемое только для чтения свойство Token, которое объявляется следующим образом. public CancellationToken Token { get; } Это и есть тот признак, который должен быть передан отменяемой задаче. Для отмены в задаче должна быть получена копия признака отмены и организо ван контроль этого признака с целью отслеживать саму отмену. Такое отслеживание можно организовать тремя способами: опросом, методом обратного вызова и с по мощью дескриптора ожидания. Проще всего организовать опрос, и поэтому здесь бу дет рассмотрен именно этот способ. С целью опроса в задаче проверяется упомянутое выше свойство IsCancellationRequested признака отмены. Если это свойство со держит логическое значение true, значит, отмена была запрошена, и задача долж на быть завершена. Опрос может оказаться весьма эффективным, если организовать его правильно. Так, если задача содержит вложенные циклы, то проверка свойства IsCancellationRequested во внешнем цикле зачастую дает лучший результат, чем его проверка на каждом шаге внутреннего цикла. Для создания задачи, из которой вызывается метод ThrowIfCancellationRequested, когда она отменяется, обычно требуется передать признак отмены как самой задаче, так и конструктору класса Task, будь то непосредственно или же косвенно через метод StartNew. Передача признака отмены самой задаче позволяет изменить состояние от меняемой задачи в запросе на отмену из внешнего кода. Далее будет использована сле дующая форма метода StartNew. public Task StartNew(Action

Перейти на страницу:

Похожие книги

Основы программирования в Linux
Основы программирования в Linux

В четвертом издании популярного руководства даны основы программирования в операционной системе Linux. Рассмотрены: использование библиотек C/C++ и стан­дартных средств разработки, организация системных вызовов, файловый ввод/вывод, взаимодействие процессов, программирование средствами командной оболочки, создание графических пользовательских интерфейсов с помощью инструментальных средств GTK+ или Qt, применение сокетов и др. Описана компиляция программ, их компоновка c библиотеками и работа с терминальным вводом/выводом. Даны приемы написания приложений в средах GNOME® и KDE®, хранения данных с использованием СУБД MySQL® и отладки программ. Книга хорошо структурирована, что делает обучение легким и быстрым. Для начинающих Linux-программистов

Нейл Мэтью , Ричард Стоунс , Татьяна Коротяева

ОС и Сети / Программирование / Книги по IT
97 этюдов для архитекторов программных систем
97 этюдов для архитекторов программных систем

Успешная карьера архитектора программного обеспечения требует хорошего владения как технической, так и деловой сторонами вопросов, связанных с проектированием архитектуры. В этой необычной книге ведущие архитекторы ПО со всего света обсуждают важные принципы разработки, выходящие далеко за пределы чисто технических вопросов.?Архитектор ПО выполняет роль посредника между командой разработчиков и бизнес-руководством компании, поэтому чтобы добиться успеха в этой профессии, необходимо не только овладеть различными технологиями, но и обеспечить работу над проектом в соответствии с бизнес-целями. В книге более 50 архитекторов рассказывают о том, что считают самым важным в своей работе, дают советы, как организовать общение с другими участниками проекта, как снизить сложность архитектуры, как оказывать поддержку разработчикам. Они щедро делятся множеством полезных идей и приемов, которые вынесли из своего многолетнего опыта. Авторы надеются, что книга станет источником вдохновения и руководством к действию для многих профессиональных программистов.

Билл де Ора , Майкл Хайгард , Нил Форд

Программирование, программы, базы данных / Базы данных / Программирование / Книги по IT
Программист-прагматик. Путь от подмастерья к мастеру
Программист-прагматик. Путь от подмастерья к мастеру

Находясь на переднем крае программирования, книга "Программист-прагматик. Путь от подмастерья к мастеру" абстрагируется от всевозрастающей специализации и технических тонкостей разработки программ на современном уровне, чтобы исследовать суть процесса – требования к работоспособной и поддерживаемой программе, приводящей пользователей в восторг. Книга охватывает различные темы – от личной ответственности и карьерного роста до архитектурных методик, придающих программам гибкость и простоту в адаптации и повторном использовании.Прочитав эту книгу, вы научитесь:Бороться с недостатками программного обеспечения;Избегать ловушек, связанных с дублированием знания;Создавать гибкие, динамичные и адаптируемые программы;Избегать программирования в расчете на совпадение;Защищать вашу программу при помощи контрактов, утверждений и исключений;Собирать реальные требования;Осуществлять безжалостное и эффективное тестирование;Приводить в восторг ваших пользователей;Формировать команды из программистов-прагматиков и с помощью автоматизации делать ваши разработки более точными.

А. Алексашин , Дэвид Томас , Эндрю Хант

Программирование / Книги по IT