Библиотека TPL C#. Параллельное программирование

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

Параллельное выполнение задач

До появление в составе .NET библиотеки TPL, чтобы организовать параллельное выполнение нескольких задач разработчику необходимо было как минимум:

  1. Организовать пул потоков для их управления
  2. Использовать различные средства для синхронизации потоков

В зависимости от сложности задач, возлагаемых на многопоточное приложение обе представленные выше задачи могли потребовать от разработчика довольно серьезных затрат времени на реализацию. Библиотека TPL позволяет, в основном, освободить разработчика от рутинных операций при работе с потоками, в частности, если обратиться к официальной документации Microsoft, то библиотека TPL позволяет:

  1. динамически масштабировать степень параллелизма для наиболее эффективного использования всех доступных процессоров
  2. осуществить секционирование работы,
  3. осуществить планирование потоков в пуле ThreadPool,
  4. осуществить поддержку отмены, управления состоянием и других низкоуровневых задачи.

Однако, за любое удобство приходится платить. И, в случае с библиотекой 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);
           
        }
    }
}

В этом случае, в консоли мы увидим следующие значения свойств потока, в котором запущена задача:

ID потока: 1
Фоновый поток: 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.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии