Содержание
В наше время — время многоядерных процессоров, параллельное программирование становится все более и более востребованной задачей при разработке программного обеспечения. В .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).