Содержание
В предыдущей части, касающейся библиотеки TPL, мы рассмотрели основные моменты по созданию и запуску задач (Task
). В этой части мы продолжим работу с объектами класса Task и более детально рассмотрим свойства и методы класса Task
.
Ожидание задач
Мы уже использовали метод ожидания выполнения задачи Wait
. Но, на этом тема организации ожидания задач не исчерпывается. Рассмотрим ещё несколько вариантов ожидания.
Ожидание всего списка задач (метод WaitAll
)
Когда необходимо, чтобы какая-либо задача запускалась только после того, как все задачи из списка (или массива) будут выполнены, удобно воспользоваться методом статичный WaitAll
:
using System; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic; namespace Tasks { internal class Program { static void Main(string[] args) { List<Task> tasks = new List<Task>(); for (int i = 0; i < 10; i++) { tasks.Add(Task.Run(() => { for (int i = 0; i < 100; i++) Console.WriteLine($"Задача {Thread.CurrentThread.ManagedThreadId} итерация {i}"); })); } Task.WaitAll(tasks.ToArray());//ожидаем пока все задачи не закончат свою работу Task task = new Task(() => { Console.WriteLine("Выполняем завершающую задачу"); }); task.Start(); task.Wait();//ожидаем выполнение задачи } } }
В этом примере мы создаем список (List
) из десяти задач, каждая из которых выводит в консоль числа о 0 до 99. Задача task
, которая создается самой последней и не входит в список будет выполнена только после того, как все задачи из списка tasks
будут выполнены. Для этого мы вызвали статический метод Task.WaitAll
в который передали массив созданных задач.
Ожидание любой задачи из списка (метод WaitAny
)
Если нам нет необходимости ожидать все задачи из списка, а достаточно дождаться завершения одной любой задачи, то можно воспользоваться методом WaitAny
. Перепишем наш пример таким образом, чтобы задача task
запускалась сразу после того, как будет завершена хотя бы одна задача из списка tasks
:
using System; using System.Threading.Tasks; using System.Threading; using System.Collections.Generic; namespace Tasks { internal class Program { static void Main(string[] args) { List<Task> tasks = new List<Task>(); for (int i = 0; i < 10; i++) { tasks.Add(Task.Run(() => { for (int i = 0; i < 100; i++) Console.WriteLine($"Задача {Thread.CurrentThread.ManagedThreadId} итерация {i}"); })); } int finished = Task.WaitAny(tasks.ToArray());//ожидаем пока все задачи не закончат свою работу Console.WriteLine($"Первая задача, которая завершила свою работу, находится в позиции {finished} в списке задач. ID {tasks[finished].Id}"); Task task = Task.Run(() => { Console.WriteLine("Выполняем завершающую задачу"); }); task.Wait();//ожидаем выполнение задачи } } }
Метод WaitAny
возвращает индекс первой завершенной задачи из переданного массива задач. Как только какая-либо задача из списка завершит свою работу, в консоль будет выведена запись, содержащая ID этой задачи и её индекс в списке. После этого будет запущена задача task
.
Возвращение результатов выполнения задач
Кроме того,что задачи могут выполняться как процедуры (без возвращаемого результата), задача также может вернуть какой-либо результат. Для этого необходимо использовать универсальный класс Task<TResult>
. Например, возьмем наш алгоритм определения простых чисел и реализуем его в виде задачи (Task
). Пусть задача будет возвращать количество простых чисел:
using System; using System.Threading.Tasks; namespace Tasks { internal class Program { static void Main(string[] args) { Task<int> primeTask = new Task<int>(()=>TaskMethod(5000)); primeTask.Start(); Console.WriteLine($"В диапазоне от 1 до 5000 обнаружено {primeTask.Result} простых чисел"); } static int TaskMethod(int N) { int count = 0; for (int i = 1; i <= N; i++) { if (IsPrime(i)) { Console.Write($"{i} "); count++; } } Console.WriteLine(); return count; } public static bool IsPrime(int number) { for (int i = 2; i < number; i++) { if (number % i == 0) return false; } return true; } } }
Здесь задача primeTask
возвращает значение типа int
— количество найденных простых чисел в заданном диапазоне. Чтобы получить возвращаемое задачей значение мы используем её свойство Result
. Аналогичным образом задача может возвращать любые необходимые нам объекты. Здесь стоит обратить внимание на три момента:
- Так как мы объявили задачу как
Task<int>
, то задача должна возвращать какое-либо целочисленное значение. - Метод, который мы определяем для задачи (
TaskMethod
) также должен возвращать объект типаint
. - Как только мы обращаемся в свойству
Result
, приложение приостанавливает выполнение главного потока и возобновляет его работу только после того, как результат (Result
) будет получен. Именно поэтому нам не потребовалось ожидать выполнение задачи и вызывать методWait
как мы это делали в предыдущей части.
Вложенные (дочерние) задачи
Дочерняя задача (или вложенная задача) — это экземпляр Task
, который создается в делегате другой задачи, которая называется родительской задачей. Например, создадим дочернюю задачу:
using System; using System.Threading.Tasks; using System.Threading; namespace Tasks { internal class Program { static void Main(string[] args) { Task mainTask = Task.Run(() => { //создаем дочернюю задачу Task.Run(() => { Console.WriteLine("Выполняем дочернюю задачу"); Thread.Sleep(1000); Console.WriteLine("Дочерняя задача выполнена"); }); Console.WriteLine("выполняем родительскую задачу"); } ); mainTask.Wait();//ожидаем выполнения родительской задачи Console.WriteLine("Задачи выполнены"); } } }
Так как порядок выполнения задач в примере не детерминирован, то в консоли мы можем увидеть такой результат:
выполняем родительскую задачу
Задачи выполнены
или такой
Выполняем дочернюю задачу
Задачи выполнены
Чтобы однозначно определить порядок выполнения родительских и дочерних задач, мы можем воспользоваться перегруженной версией конструктора для Task
и присоединить дочернюю задачу к родительской. Перепишем пример следующим образом:
static void Main(string[] args) { Task mainTask = Task.Factory.StartNew(() => { Console.WriteLine("Родительская задача начинает свою работу"); //создаем дочернюю задачу var inner = Task.Factory.StartNew(() => { Console.WriteLine("Выполняем дочернюю задачу"); Thread.Sleep(2000); Console.WriteLine("Дочерняя задача выполнена"); }, TaskCreationOptions.AttachedToParent);//присоединяем задачу } ); mainTask.Wait();//ожидаем выполнения родительской задачи Console.WriteLine("Задачи выполнены"); }
Здесь мы передали в конструкторе дочерней задачи второй параметр — TaskCreationOptions.AttachedToParent
, который указывает, что дочерняя задача будет прикреплена к родительской. В этом случае родительская задача не завершит свою работу до тех пор, пока не будет выполнена дочерняя задача (или даже массив вложенных задач).
Итого
В этой части мы более подробно рассмотрели работу с классом Task
— научились выполнять ожидание задач, создавать вложенные задачи и присоединять их к родительским. В следующей части разберемся с тем как создавать цепочки взаимосвязанных задач.