Библиотека TPL C#. Работа с Task

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

В предыдущей части, касающейся библиотеки 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. Аналогичным образом задача может возвращать любые необходимые нам объекты. Здесь стоит обратить внимание на три момента:

  1. Так как мы объявили задачу как Task<int>, то задача должна возвращать какое-либо целочисленное значение.
  2. Метод, который мы определяем для задачи (TaskMethod) также должен возвращать объект типа int.
  3. Как только мы обращаемся в свойству 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 — научились выполнять  ожидание задач, создавать вложенные задачи и присоединять их к родительским. В следующей части разберемся с тем как создавать цепочки взаимосвязанных задач.

уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
guest
0 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии