Содержание
В наше время — время многоядерных процессоров, параллельное программирование становится все более и более востребованной задачей при разработке программного обеспечения. В .NET Framework 4 была добавлена удобная библиотека для параллельного программирования — Task Parallel library (библиотека параллельных задач) или, сокращенно, TPL. Так, если ранее нам приходилось тратить достаточно много времени на разработку потоков (Thread), выполнение взаимоблокировок и прочих задач, обеспечивающих эффективное и безопасное параллельное выполнение нескольких задач, то с TPL мы получили удобный и эффективный инструмент для параллельного программирования в C#, позволяющий разрабатывать программное обеспечение, максимально использующее все возможности современных компьютеров.
Класс Task
Собственно, из самого названия библиотеки (TPL) понятно, что в основе всей работы лежит понятие «задача». Задача в библиотеке классов .NET представлена классом Task
, который находится в пространстве имен System.Threading.Tasks
. Так как работа, выполняемая объектом Task
, выполняется чаще всего асинхронно в потоке пула потоков, а не синхронно в основном потоке приложения, то для определения состояния задачи могут использоваться такие свойства Task
как Status
, IsCanceled
, IsCompleted
и 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
, IsCanceled
, IsCompleted
и 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(); }
Обратите внимание на три момента:
- Действие (
Action
) задачи task длиться не менее 3 секунд; - Метод
Wait()
вызван с параметром500
, который означает, что ждать задачу мы будем0.5 с
- После того, как время ожидания вышло — мы получаем статус задачи и выводим строку в консоль, если задача ещё не завершена.
Вывод в консоли в итоге будет следующий:
LongAction ещё выполняется
LongAction выполнена, запускаем остальные задачи
End Main()
task2 Task=2, Thread=5
task3 Task=3, Thread=6
task4 Task=4, Thread=8
LongAction
Как видите, здесь мы получили статус задачи, а сама задача task
в виду её длительности завершается последней из всех. Что касается других свойств Task
, то они работают аналогичным образом, то есть:
Is
— указывает завершилось ли выполнение данного экземпляраCanceled Task
из-за отменыIs
— указывает завершена ли задачаCompleted Is
— указывает выполнена ли задачаCompleted Successfully Is
— указывает завершилась ли задачаFaulted Task
из-за необработанного исключения.
Итого
Сегодня мы, в общих чертах, познакомились с тем как работать с задачами при параллельном программировании в C# с использованием библиотеки TPL. Механизм использования задач (Task) позволяет достаточно просто и интуитивно понятно организовать параллельное выполнение нескольких задач в вашей программе, не прибегая к использованию потоков (Thread).