355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Герберт Шилдт » C# 4.0: полное руководство » Текст книги (страница 71)
C# 4.0: полное руководство
  • Текст добавлен: 6 апреля 2017, 04:00

Текст книги "C# 4.0: полное руководство"


Автор книги: Герберт Шилдт



сообщить о нарушении

Текущая страница: 71 (всего у книги 83 страниц)

Применение методов ожидания

В приведенных выше примерах основной поток исполнения, а по существу, метод 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() #2, подсчет равен 1

В методе MyTask() #1, подсчет равен 1

В методе MyTask() #2, подсчет равен 2

В методе MyTask() #1, подсчет равен 2

В методе MyTask() #2, подсчет равен 3

В методе MyTask() #1, подсчет равен 3

В методе MyTask() #2, подсчет равен 4

В методе MyTask() #1, подсчет равен 4

В методе MyTask() #2, подсчет равен 5

В методе MyTask() #1, подсчет равен 5

В методе MyTask() #2, подсчет равен 6

В методе MyTask() #1, подсчет равен 6

В методе MyTask() #2, подсчет равен 7

В методе MyTask() #1, подсчет равен 7

В методе MyTask() #2, подсчет равен 8

В методе MyTask() #1, подсчет равен 8

В методе MyTask() #2, подсчет равен 9

MyTask №2 завершен

В методе MyTask() #1, подсчет равен 9

MyTask №1 завершен

Основной поток завершен.

Как следует из приведенного выше результата, выполнение метода 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)

В данном случае обобщенный параметр Т обозначает класс 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(FuncCObject, TResult> функция, 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 SumIt(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(SumIt, 3);

    Console.WriteLine("Результат после выполнения задачи SumIt: "

             + tsk2.Result);

    tsk.Dispose();

    tsk2.Dispose();

    Console.WriteLine(«Основной поток завершен.»);

  }

}

Выполнение этой программы приводит к следующему результату.

Основной поток запущен.

Результат после выполнения задачи MyTask: True

Результат после выполнения SumIt: 6

Основной поток завершен.

Помимо упомянутых выше форм класса Task и метода StartNew, имеются также другие формы. Они позволяют указывать другие дополнительные параметры.


    Ваша оценка произведения:

Популярные книги за неделю