Методы ожидания задачи

63

В приведенных примерах из предыдущей статьи основной поток исполнения, а по существу, метод Main(), завершался потому, что такой результат гарантировали вызовы метода Thread.Sleep(). Но подобный подход нельзя считать удовлетворительным.

Организовать ожидание завершения задач можно и более совершенным способом, применяя методы ожидания, специально предоставляемые в классе Task. Самым простым из них считается метод Wait(), приостанавливающий исполнение вызывающего потока до тех пор, пока не завершится вызываемая задача.

При выполнении этого метода могут быть сгенерированы два исключения. Первым из них является исключение ObjectDisposedException. Оно генерируется в том случае, если задача освобождена посредством вызова метода Dispose(). А второе исключение, AggregateException, генерируется в том случае, если задача сама генерирует исключение или же отменяется. Как правило, отслеживается и обрабатывается именно это исключение.

В связи с тем что задача может сгенерировать не одно исключение, если, например, у нее имеются порожденные задачи, все подобные исключения собираются в единое исключение типа AggregateException. Для того чтобы выяснить, что же произошло на самом деле, достаточно проанализировать внутренние исключения, связанные с этим совокупным исключением. А до тех пор в приведенных далее примерах любые исключения, генерируемые задачами, будут обрабатываться во время выполнения.

Ниже приведен вариант программы из предыдущей статьи, измененный с целью продемонстрировать применение метода Wait() на практике. Этот метод используется внутри метода Main(), чтобы приостановить его выполнение до тех пор, пока не завершатся обе задачи task1 и task2:

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

namespace ConsoleApplication1
{
    class Program
    {
        // Метод выполняемый в качестве задачи
        static void MyTask()
        {
            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);
        }

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

            Task task1 = new Task(MyTask);
            Task task2 = new Task(MyTask);

            // Запустить задачу
            task1.Start();
            task2.Start();

            // Приостановить выполнение метода Main() пока обе задачи не завершатся
            task1.Wait();
            task2.Wait();

            Console.WriteLine("Основной поток завершен");
            Console.ReadLine();
        }
    }
}
Процесс ожидания задачи

Как следует из приведенного выше результата, выполнение метода Main() приостанавливается до тех пор, пока не завершатся обе задачи task1 и task2. Следует, однако, иметь в виду, что в рассматриваемой здесь программе последовательность завершения задач task и task2 не имеет особого значения для вызовов метода Wait(). Так, если первой завершается задача task2, то в вызове метода task1.Wait() будет по-прежнему ожидаться завершение задачи task1. В таком случае вызов метода task2.Wait() приведет к выполнению и немедленному возврату из него, поскольку задача task2 уже завершена.

В данном случае оказывается достаточно двух вызовов метода Wait(), но того же результата можно добиться и более простым способом, воспользовавшись методом WaitAll(). Этот метод организует ожидание завершения группы задач. Возврата из него не произойдет до тех пор, пока не завершатся все задачи.

Задачи, завершения которых требуется ожидать, передаются с помощью параметра в виде массива tasks. А поскольку этот параметр относится к типу params, то данному методу можно отдельно передать массив объектов типа Task или список задач. При этом могут быть сгенерированы различные исключения, включая и AggregateException.

/* task1.Wait();
task2.Wait();*/
            
Task.WaitAll(task1, task2);

Организуя ожидание завершения нескольких задач, следует быть особенно внимательным, чтобы избежать взаимоблокировок. Так, если две задачи ожидают завершения друг друга, то вызов метода WaitAll() вообще не приведет к возврату из него.

Разумеется, условия для взаимоблокировок возникают в результате ошибок программирования, которых следует избегать. Следовательно, если вызов метода WaitAll() не приводит к возврату из него, то следует внимательно проанализировать, могут ли две задачи или больше взаимно блокироваться. (Вызов метода Wait(), который не приводит к возврату из него, также может стать причиной взаимоблокировок.)

Иногда требуется организовать ожидание до тех пор, пока не завершится любая из группы задач. Для этой цели служит метод WaitAny(). Ниже приведена простейшая форма его объявления:

public static int WaitAny(params Task[] tasks)

Задачи, завершения которых требуется ожидать, передаются с помощью параметра в виде массива tasks объектов типа Task или отдельного списка аргументов типа Task. Этот метод возвращает индекс задачи, которая завершается первой. При этом могут быть сгенерированы различные исключения. Попробуйте применить метод WaitAny() на практике, подставив в предыдущей программе следующий вызов:

Task.WaitAny(task1, task2);

Теперь, выполнение метода Main() возобновится, а программа завершится, как только завершится одна из двух задач. Помимо рассматривавшихся здесь форм методов Wait(), WaitAll() и WaitAny(), имеются и другие их варианты, в которых можно указывать период простоя или отслеживать признак отмены.

Метод Dispose()

В классе Task реализуется интерфейс IDisposable, в котором определяется метод Dispose(). Ниже приведена форма его объявления:

public void Dispose()

Метод Dispose() реализуется в классе Task, освобождая ресурсы, используемые этим классом. Как правило, ресурсы, связанные с классом Task, освобождаются автоматически во время "сборки мусора" (или по завершении программы). Но если эти ресурсы требуется освободить еще раньше, то для этой цели служит метод Dispose(). Это особенно важно в тех программах, где создается большое число задач, оставляемых на произвол судьбы.

Следует, однако, иметь в виду, что метод Dispose() можно вызывать для отдельной задачи только после ее завершения. Следовательно, для выяснения факта завершения отдельной задачи, прежде чем вызывать метод Dispose(), потребуется некоторый механизм, например, вызов метода Wait(). Именно поэтому так важно было рассмотреть метод Wait(), перед тем как обсуждать метод Dispose(). Если же попытаться вызвать Dispose() для все еще активной задачи, то будет сгенерировано исключение InvalidOperationException.

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