Работа с задачами

77

Класс TaskFactory

Приведенные ранее примеры программ были составлены не так эффективно, как следовало бы, поскольку задачу можно создать и сразу же начать ее исполнение, вызвав метод StartNew(), определенный в классе TaskFactory. В классе TaskFactory предоставляются различные методы, упрощающие создание задач и управление ими.

По умолчанию объект класса TaskFactory может быть получен из свойства Factory, доступного только для чтения в классе Task. Используя это свойство, можно вызвать любые методы класса TaskFactory. Метод StartNew() существует во множестве форм. Ниже приведена самая простая форма его объявления:

public Task StartNew(Action action)

где action — точка входа в исполняемую задачу. Сначала в методе StartNew() автоматически создается экземпляр объекта типа Task для действия, определяемого параметром action, а затем планируется запуск задачи на исполнение. Следовательно, необходимость в вызове метода Start() теперь отпадает.

Ниже представлены различные способы запуска задач:

// Использование фабрики задач
TaskFactory tf = new TaskFactory();
Task t1 = tf.StartNew(MyTask);

// Использование фабрики задач через задачу
Task t2 = Task.Factory.StartNew(MyTask);

// Использование конструктора Task
Task task1 = new Task(MyTask);
task1.Start();

Применение лямбда-выражения в качестве задачи

Кроме использования обычного метода в качестве задачи, существует и другой, более рациональный подход: указать лямбда-выражение как отдельно решаемую задачу.

Напомним, что лямбда-выражения являются особой формой анонимных функций. Поэтому они могут исполняться как отдельные задачи. Лямбда-выражения оказываются особенно полезными в тех случаях, когда единственным назначением метода является решение одноразовой задачи. Лямбда-выражения могут составлять отдельную задачу или же вызывать другие методы. Так или иначе, применение лямбда-выражения в качестве задачи может стать привлекательной альтернативой именованному методу.

В приведенном ниже примере программы демонстрируется применение лямбда-выражения в качестве задачи. В этой программе код метода MyTask() из предыдущих примеров программ преобразуется в лямбда-выражение:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Основной поток запущен");

            // Используем лямбда-выражение
            Task task1 = Task.Factory.StartNew( () => {
                Console.WriteLine("MyTask() №{0} запущен", Task.CurrentId);

                for (int count = 0; count < 10; count++)
                {
                    Thread.Sleep(500);
                    Console.WriteLine("В методе MyTask №{0} подсчет равен {1}", Task.CurrentId, count);
                }

                Console.WriteLine("MyTask() #{0} завершен", Task.CurrentId);
            });

            task1.Wait();
            task1.Dispose();

            Console.WriteLine("Основной поток завершен");
            Console.ReadLine();
        }
    }
}
Использование лямбда-выражения для создания задачи

Помимо применения лямбда-выражения для описания задачи, обратите также внимание в данной программе на то, что вызов метода task1.Dispose() не делается до тех пор, пока не произойдет возврат из метода task1.Wait(). Как пояснялось в предыдущей статье, метод Dispose() можно вызывать только по завершении задачи. Для того чтобы убедиться в этом, попробуйте поставить вызов метода task1.Dispose() в рассматриваемой здесь программе перед вызовом метода task1.Wait(). Вы сразу же заметите, что это приведет к исключительной ситуации.

Создание продолжения задачи

Одной из новаторских и очень удобных особенностей библиотеки TPL является возможность создавать продолжение задачи. Продолжение — это одна задача, которая автоматически начинается после завершения другой задачи. Создать продолжение можно, в частности, с помощью метода ContinueWith(), определенного в классе Task. Ниже приведена простейшая форма его объявления:

public Task ContinueWith(Action<Task> действие_продолжения)

где действие_продолжения обозначает задачу, которая будет запущена на исполнение по завершении вызывающей задачи. У делегата Action имеется единственный параметр типа Task. Следовательно, вариант делегата Action, применяемого в данном методе, выглядит следующим образом:

public delegate void Action<T>(T obj)

В данном случае обобщенный параметр T обозначает класс Task.

Возврат значения из задачи

Задача может возвращать значение. Это очень удобно по двум причинам. Во-первых, это означает, что с помощью задачи можно вычислить некоторый результат. Подобным образом поддерживаются параллельные вычисления. И во-вторых, вызывающий процесс окажется блокированным до тех пор, пока не будет получен результат. Это означает, что для организации ожидания результата не требуется никакой особой синхронизации.

Для того чтобы возвратить результат из задачи, достаточно создать эту задачу, используя обобщенную форму Task<TResult> класса Task. Ниже приведены два конструктора этой формы класса Task:

public Task(Func<TResult> функция)
public Task(Func<Object, TResult> функция, Object состояние)

где функция обозначает выполняемый делегат. Обратите внимание на то, что он должен быть типа Func, а не Action. Тип Func используется именно в тех случаях, когда задача возвращает результат. В первом конструкторе создается задача без аргументов, а во втором конструкторе — задача, принимающая аргумент типа Object, передаваемый как состояние. Имеются также другие конструкторы данного класса.

Как и следовало ожидать, имеются также другие варианты метода StartNew(), доступные в обобщенной форме класса TaskFactory<TResult> и поддерживающие возврат результата из задачи. Ниже приведены те варианты данного метода, которые применяются параллельно с только что рассмотренными конструкторами класса Task:

public Task<TResult> StartNew(Func<TResult> функция)
public Task<TResult> StartNew(Func<Object, TResult> функция, Object состояние)

В любом случае значение, возвращаемое задачей, получается из свойства Result в классе Task, которое определяется следующим образом:

public TResult Result { get; internal set; }

Аксессор set является внутренним для данного свойства, и поэтому оно оказывается доступным во внешнем коде, по существу, только для чтения. Следовательно, задача получения результата блокирует вызывающий код до тех пор, пока результат не будет вычислен.

Пройди тесты
Лучший чат для C# программистов