Содержание
Основная цель Task Parallel Library (TPL, библиотека параллельных задач) — повышение производительности разработчиков при добавлении многопоточности в приложения C#. Начиная с .NET Framework 4 наиболее предпочтительным вариантом использования многопоточности в приложении является использование именно библиотек TPL, но, при этом, такие классы как Thread
, который мы рассматривали в предыдущей части, также могут использоваться и все также находят широкое применение на сегодняшний день.
Параллельное выполнение задач
До появление в составе .NET библиотеки TPL, чтобы организовать параллельное выполнение нескольких задач разработчику необходимо было как минимум:
- Организовать пул потоков для их управления
- Использовать различные средства для синхронизации потоков
В зависимости от сложности задач, возлагаемых на многопоточное приложение обе представленные выше задачи могли потребовать от разработчика довольно серьезных затрат времени на реализацию. Библиотека TPL позволяет, в основном, освободить разработчика от рутинных операций при работе с потоками, в частности, если обратиться к официальной документации Microsoft, то библиотека TPL позволяет:
- динамически масштабировать степень параллелизма для наиболее эффективного использования всех доступных процессоров
- осуществить секционирование работы,
- осуществить планирование потоков в пуле
ThreadPool
, - осуществить поддержку отмены, управления состоянием и других низкоуровневых задачи.
Однако, за любое удобство приходится платить. И, в случае с библиотекой TPL, наша плата — это затраты времени на изучение новой концепции, заложенной в основу библиотеки TPL.
Задача (Task)
Основная концепция библиотеки TPL — использование задач (классTask
располагается в пространстве имен System.Threading.Tasks
).
Задача — это какая-либо отдельная операция, которая должна выполняться параллельно.
В зависимости от потребностей, мы можем запускать задачи как в отдельном потоке из пула потоков, так и в главном потоке приложения (синхронно). Рассмотрим различные варианты запуска задач в C#.
Использование метода Start
Метод Start
запускает выполнение предварительно созданной задачи:
using System; using System.Threading.Tasks; namespace Tasks { internal class Program { static void Main(string[] args) { Task task = new Task(Counter); task.Start(); Console.ReadLine(); } public static void Counter() { for (int i = 0; i < 10; i++) Console.WriteLine(i); Console.WriteLine("Счётчик закончил свою работу"); } } }
Здесь для создания новой задачи (Task
) мы используем конструктор, который в качестве параметра принимает делегат Action
:
public delegate void Action();
т.е. в конструкторе мы можем предать любой метод, соответствующий сигнатуре делегата. Таким у нас является метод Counter
. После запуска задачи на выполнение методом Start
задача будет выполнена в фоновом потоке, который будет взят из пула потоков. Убедиться в этом можно, используя свойства класса Thread
, например, дописав метод Counter
следующим образом:
public static void Counter() { Console.WriteLine($"ID потока: {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"Фоновый поток: {Thread.CurrentThread.IsBackground}"); Console.WriteLine($"Поток взят из пула потоков: {Thread.CurrentThread.IsThreadPoolThread}"); for (int i = 0; i < 10; i++) Console.WriteLine(i); }
Теперь запустим приложение и посмотрим на значения свойств текущего потока:
ID потока: 4 Фоновый поток: True Поток взят из пула потоков: True
Использование статического метода Task.Factory.StartNew
В этом случае, мы также передаем в метод StartNew
делегат типа Action
:
Task task = Task.Factory.StartNew(Counter);
Результат будет тот же, что и в предыдущем примере — задача запустится в отдельном фоновом потоке.
Использование статического метода Run
Task task = Task.Run(Counter);
Все три способа запуска задач, по сути, идентичны — выполнение задачи осуществляется в отдельном фоновом потоке из пула потоков. Четвертый способ отличается тем, что задача запускается синхронно в главном потоке приложения
Использование метода RunSynchronously
Метод RunSynchronously
выполняет запуск задачи в главном потоке приложения синхронно:
using System; using System.Threading.Tasks; using System.Threading; namespace Tasks { internal class Program { static void Main(string[] args) { Task task = new Task(Counter); task.RunSynchronously(); //запуск задачи синхронно Console.ReadLine(); } public static void Counter() { Console.WriteLine($"ID потока: {Thread.CurrentThread.ManagedThreadId}"); Console.WriteLine($"Фоновый поток: {Thread.CurrentThread.IsBackground}"); Console.WriteLine($"Поток взят из пула потоков: {Thread.CurrentThread.IsThreadPoolThread}"); for (int i = 0; i < 10; i++) Console.WriteLine(i); } } }
В этом случае, в консоли мы увидим следующие значения свойств потока, в котором запущена задача:
Фоновый поток: False
Поток взят из пула потоков: False
Ожидание задач
Рассмотрим следующий пример, который запускает задачу, используя метод Start
:
static void Main(string[] args) { Task task = new Task(()=>Console.WriteLine("Привет Task")); task.Start(); Console.WriteLine("Приложение завершило свою работу"); }
В результате, мы можем наблюдать вот такой вывод консоли:
Привет Task
Строка «Приложение завершило свою работу» говорит нам о том, что главный поток приложения уже дошел до конца метода Main
, но, при этом, сама задача была выполнена позже, хотя Start
мы вызывали ДО вывода в консоль строки из главного потока. Такое поведение связано, в первую очередь, с самой организацией многопоточной работы в .NET — операционная система сама решает, в зависимости от наличия ресурсов, какой поток работать в данный момент времени (об этом мы говорили здесь).
Для большей наглядности, приведем ещё один пример:
using System; using System.Threading.Tasks; using System.Threading; namespace Tasks { internal class Program { static void Main(string[] args) { Task task = new Task(Counter); task.Start(); } public static void Counter() { for (int i = 0; i < 10; i++) Console.WriteLine(i); } } }
Здесь мы ожидаем, что на экран будут выведены числа с 0 до 9, а по факту — приложение завершит свою работу, не выведя на экран ни одного числа. Такое поведение, опять же, мы обсуждали и связано оно с тем, что поток, в котором выполняется задача, является фоновым (IsBackground=True
). Чтобы задача из примера выше была выполнена, нам необходимо выполнить ожидание задачи. В библиотеке TPL это делается следующим образом:
using System; using System.Threading.Tasks; using System.Threading; namespace Tasks { internal class Program { static void Main(string[] args) { Task task = new Task(Counter); task.Start(); task.Wait();//ожидаем выполнение задачи } public static void Counter() { for (int i = 0; i < 10; i++) Console.WriteLine(i); } } }
Теперь можно запустить приложение и убедиться, что на экран будут выведены цифры от 0 до 9 и только после этого приложение завершит свою работу.
Итого
Сегодня мы рассмотрели основные моменты по работе с библиотекой TPL в .NET C#. В основе работы библиотеки лежит понятие «задачи», которая может выполняться как в отдельном фоновом потоке из пула потоков, так и в главном потоке приложения синхронно. Для запуска задач могут использоваться различные способы, которые мы рассмотрели выше. На этом вопросы по работе с TPL не заканчиваются и в следующей части мы более подробно остановимся на работе с задачами в TPL.