using System;
using System.Threading;
using ThreadPoolApp;
Console.WriteLine("***** Fun with the .NET Core Runtime Thread Pool *****\n");
Console.WriteLine("Main thread started. ThreadID = {0}",
Thread.CurrentThread.ManagedThreadId);
Printer p = new Printer();
WaitCallback workItem = new WaitCallback(PrintTheNumbers);
// Поставить в очередь метод десять раз.
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(workItem, p);
}
Console.WriteLine("All tasks queued");
Console.ReadLine();
static void PrintTheNumbers(object state)
{
Printer task = (Printer)state;
task.PrintNumbers();
}
У вас может возникнуть вопрос: почему взаимодействовать с пулом потоков, поддерживаемым средой .NET Core Runtime, выгоднее по сравнению с явным созданием объектов
Thread
? Использование пула потоков обеспечивает следующие преимущества.
• Пул потоков эффективно управляет потоками, сводя к минимуму количество потоков, которые должны создаваться, запускаться и останавливаться.
• За счет применения пула потоков можно сосредоточиться на решении задачи, а не на потоковой инфраструктуре приложения.
Тем не менее, в некоторых случаях ручное управление потоками оказывается более предпочтительным. Ниже приведены примеры.
• Когда требуются потоки переднего плана или должен устанавливаться приоритет потока. Потоки из пула всегда являются фоновыми и обладают стандартным приоритетом (
ThreadPriority.Normal
).
• Когда требуется поток с фиксированной идентичностью, чтобы его можно было прерывать, приостанавливать или находить по имени.
На этом исследование пространства имен
System.Threading
завершено. Несомненно, понимание вопросов, рассмотренных в настоящей главе до сих пор (особенно в разделе, посвященном проблемам параллелизма), будет чрезвычайно ценным при создании многопоточного приложения. А теперь, опираясь на имеющийся фундамент, мы переключим внимание на несколько новых аспектов, связанных с потоками, которые появились в .NET 4.0 и остались в .NET Core. Для начала мы обратимся к альтернативной потоковой модели под названием TPL.
Параллельное программирование с использованием TPL
Вы уже ознакомились с объектами из пространства имен
System.Threading
, которые позволяют строить многопоточное программное обеспечение. Начиная с версии .NET 4.0, в Microsoft ввели новый подход к разработке многопоточных приложений, предусматривающий применение библиотеки параллельного программирования, которая называется TPL. С помощью типов из
System.Threading.Tasks
можно строить мелкомодульный масштабируемый параллельный код без необходимости напрямую иметь дело с потоками или пулом потоков.
Однако речь не идет о том, что вы не будете использовать типы из пространства имен
System.Threading
во время применения TPL. Оба инструментальных набора для создания многопоточных приложений могут вполне естественно работать вместе. Сказанное особенно верно в связи с тем, что пространство имен
System.Threading
по-прежнему предоставляет большинство примитивов синхронизации, которые рассматривались ранее (
Monitor
,
Interlocked
и т.д.). В итоге вы на самом деле обнаружите, что иметь дело с библиотекой TPL предпочтительнее, чем с первоначальным пространством имен
System.Threading
, т.к. те же самые задачи могут решаться гораздо проще.
Пространство имен System.Threading.Tasks
Все вместе типы из пространства
System.Threading.Tasks
называются
библиотекой параллельных задач (Task Parallel Library — TPL). Библиотека TPL будет автоматически распределять нагрузку приложения между доступными процессорами в динамическом режиме с применением пула потоков исполняющей среды. Библиотека TPL поддерживает разбиение работы на части, планирование потоков, управление состоянием и другие низкоуровневые детали. В конечном итоге появляется возможность максимизировать производительность приложений .NET Core, не сталкиваясь со сложностями прямой работы с потоками.
Роль класса Parallel
Основным классом в TPL является
System.Threading.Tasks.Parallel
. Он содержит методы, которые позволяют осуществлять итерацию по коллекции данных (точнее по объекту, реализующему интерфейс
IEnumerable<T>
) в параллельной манере. Это делается главным образом посредством двух статических методов
Parallel.For()
и
Parallel.ForEach()
, каждый из которых имеет множество перегруженных версий.
Упомянутые методы позволяют создавать тело из операторов кода, которое будет выполняться в параллельном режиме. Концептуально такие операторы представляют логику того же рода, которая была бы написана в нормальной циклической конструкции (с использованием ключевых слов
for
и
foreach
языка С#). Преимущество заключается в том, что класс
Parallel
будет самостоятельно извлекать потоки из пула потоков (и управлять параллелизмом).
Оба метода требуют передачи совместимого с
IEnumerable
или
IEnumerable<T>
контейнера, который хранит данные, подлежащие обработке в параллельном режиме. Контейнер может быть простым массивом, необобщенной коллекцией (вроде
ArrayList
), обобщенной коллекцией (наподобие
List<T>
) или результатами запроса LINQ.
Вдобавок понадобится применять делегаты
System.Func<T>
и
System.Action<T>
для указания целевого метода, который будет вызываться при обработке данных. Делегат
Func<T>
уже встречался в главе 13 во время исследования технологии LINQ to Objects. Вспомните, что
Func<T>
представляет метод, который возвращает значение и принимает различное количество аргументов. Делегат
Action<T>
похож на
Func<T>
в том, что позволяет задавать метод, принимающий несколько параметров, но данный метод должен возвращать
void
.