Параллельное программирование в C#: создание и выполнение задач (Task)

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

В наше время — время многоядерных процессоров, параллельное программирование становится все более  и  более востребованной задачей при разработке программного обеспечения. В .NET Framework 4 была добавлена удобная библиотека для параллельного программирования — Task Parallel library (библиотека параллельных задач) или, сокращенно, TPL.  Так, если ранее нам приходилось тратить достаточно много времени на разработку потоков (Thread), выполнение взаимоблокировок и прочих задач, обеспечивающих эффективное и безопасное параллельное выполнение нескольких задач, то с TPL мы получили удобный и эффективный инструмент для параллельного программирования в C#, позволяющий разрабатывать программное обеспечение, максимально использующее все  возможности современных компьютеров.

Класс Task

Собственно, из самого названия библиотеки (TPL) понятно, что в основе всей работы лежит понятие «задача».  Задача в библиотеке классов .NET представлена классом Task, который находится в пространстве имен System.Threading.Tasks. Так как работа, выполняемая объектом Task,  выполняется чаще всего асинхронно в потоке пула потоков, а не синхронно в основном потоке приложения, то для определения состояния задачи могут использоваться такие свойства Task как Status, IsCanceledIsCompleted и IsFaulted.

Варианты определение и запуска задач Task

Разработчики .NET предусмотрели различные варианты определения и запуска задач. В зависимости от ваших предпочтений и потребностей можно использовать один из четырех вариантов.

1. Вызова конструктора задачи и запуск путем вызова метода Start()

Task task = new Task(() => Console.WriteLine("Hello TPL"));
task.Start();

В качестве параметра в конструкторе Task используется делегат Action:

public delegate void Action();

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

public static void MyAction()
{
    Console.WriteLine("Action");
}

static void Main(string[] args)
{
    Task task2 = new Task(MyAction);
    task2.Start();

    Console.ReadLine();
}

Таким образом, в примере выше, в консоль будет выведена строка «Action», хотя, никто не запрещает вам наполнить метод MyAction какими-либо более сложными действиями.

2. Создание задачи и её запуск с использованием метода StartNew()

Второй способ заключается в использовании статического метода StartNew() класса Task. Пример ниже демонстрирует создание и запуск задачи с использованием StartNew():

public static void MyAction()
{
    Console.WriteLine("Action");
}

static void Main(string[] args)
{
    Task task3 = Task.Factory.StartNew(MyAction);
}

3. Создание задачи и её запуск с использованием метода Run()

Третий способ создания и запуска задачи — использование метода Run():

public static void MyAction()
{
    Console.WriteLine("Action");
}

static void Main(string[] args)
{
    Task task4 = Task.Run(MyAction);
}

4. Создание задачи и её синхронный запуск с использованием метода RunSynchronously()

Не исключено, что в какой-то момент вам окажется необходимым запустить задачу синхронно в основном потоке приложения. Для таких целей можно использовать метод RunSynchronously().

public static void MyAction()
{
    Console.WriteLine("Action");
}

static void Main(string[] args)
{
    Task task5 = new Task(MyAction);
    task5.RunSynchronously();
}

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

Выполнение и ожидание выполнения задач

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

static void Main(string[] args)
{
    Console.WriteLine("Start Main()");
    
    Task task =  Task.Run(() => Console.WriteLine($"task  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task2 = Task.Run(() => Console.WriteLine($"task2  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task3 = Task.Run(() => Console.WriteLine($"task3  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task4 = Task.Run(() => Console.WriteLine($"task4  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    
    Console.WriteLine("End Main()");
    Console.ReadLine();
}

В итоге, в консоли можно увидеть следующие строки:

Start Main()

End Main()

task2 Task=2, Thread=6

task Task=1, Thread=5

task4 Task=4, Thread=4

task3 Task=3, Thread=7

которые свидетельствуют как раз о том, что было сказано выше — никто не гарантирует порядок выполнения задач такой, в котором эти задачи создавались. Однако, это совсем не означает, что при параллельном программировании в C# мы лишены такой возможности, как управление порядком выполнения задач. Мы можем синхронизировать выполнение вызывающего потока и асинхронных задач (Task), которые он запускает, вызвав метод Wait, чтобы дождаться завершения одной или нескольких задач. Рассмотрим пример, когда вызывающий поток блокируется до тех пор, пока не выполнится определенная задача:

public static void LongAction()
{
    Thread.Sleep(3000);
    Console.WriteLine("LongAction");
}

static void Main(string[] args)
{
    Console.WriteLine("Start Main()");
    
    Task task = Task.Run(LongAction);
    task.Wait(); //ждем, пока задача не будет полностью выполнена  
    Console.WriteLine("LongAction выполнена, запускаем остальные задачи");
    Task task2 = Task.Run(() => Console.WriteLine($"task2  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task3 = Task.Run(() => Console.WriteLine($"task3  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task4 = Task.Run(() => Console.WriteLine($"task4  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    
    Console.WriteLine("End Main()");
    Console.ReadLine();
}

В этом примере все task2, task3 и task4 не будут запущены пока полностью не  выполнится задача task. Метод Wait() имеет ряд перегруженных версий, которые позволяют настроить ожидание, например, задать интервал в течение которого будет происходить ожидание. Узнать статус (состояние задачи) можно, используя свойства Status, IsCanceledIsCompleted и IsFaulted. Например, напишем вот такой код:

public static void LongAction()
{
    Thread.Sleep(3000);
    Console.WriteLine("LongAction");
}

static void Main(string[] args)
{
    Console.WriteLine("Start Main()");
    
    Task task = Task.Run(LongAction);
    task.Wait(500); //ждем, пока задача не будет полностью выполнена  

    if (task.Status == TaskStatus.Running)
        Console.WriteLine("LongAction ещё выполняется");

    Console.WriteLine("LongAction выполнена, запускаем остальные задачи");
    Task task2 = Task.Run(() => Console.WriteLine($"task2  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task3 = Task.Run(() => Console.WriteLine($"task3  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    Task task4 = Task.Run(() => Console.WriteLine($"task4  Task={Task.CurrentId}, Thread={Thread.CurrentThread.ManagedThreadId}"));
    
    Console.WriteLine("End Main()");
    Console.ReadLine();
}

Обратите внимание на три момента:

  1. Действие (Action) задачи task длиться не менее 3 секунд;
  2. Метод Wait() вызван с параметром 500, который означает, что ждать задачу мы будем 0.5 с
  3.  После того, как время ожидания вышло — мы получаем статус задачи и выводим строку в консоль, если задача ещё не завершена.

Вывод в консоли в итоге будет следующий:

Start Main()

LongAction ещё выполняется

LongAction выполнена, запускаем остальные задачи

End Main()

task2 Task=2, Thread=5

task3 Task=3, Thread=6

task4 Task=4, Thread=8

LongAction

Как видите, здесь мы получили статус задачи, а сама задача task в виду её длительности завершается последней из всех. Что касается других свойств Task, то они работают аналогичным образом, то есть:

  • IsCanceled — указывает завершилось ли выполнение данного экземпляра Task из-за отмены
  • IsCompleted — указывает завершена ли задача
  • IsCompletedSuccessfully — указывает выполнена ли задача
  • IsFaulted — указывает завершилась ли задача Task из-за необработанного исключения.

Итого

Сегодня мы, в общих чертах, познакомились с тем как работать с задачами при параллельном программировании в C# с использованием библиотеки TPL. Механизм использования задач (Task) позволяет достаточно просто и интуитивно понятно организовать параллельное выполнение нескольких задач в вашей программе, не прибегая к использованию потоков (Thread).

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