Шрифт:
Интервал:
Закладка:
Глава начинается с определения общей природы "многопоточного приложения". Затем будет представлено первоначальное пространство имен для многопоточности, поставляемое со времен версии .NET 1.0 и называемое System.Threading. Вы ознакомитесь с многочисленными типами (Thread, ThreadStart и т.д.), которые позволяют явно создавать дополнительные потоки выполнения и синхронизировать разделяемые ресурсы, обеспечивая совместное использование данных несколькими потоками в неизменчивой манере.
В оставшихся разделах главы будут рассматриваться три более новых технологии, которые разработчики приложений .NET Core могут применять для построения многопоточного программного обеспечения: библиотека параллельных задач (Task Parallel Library — TPL), технология PLINQ (Parallel LINQ — параллельный LINQ) и появившиеся относительно недавно (в версии C# 6) ключевые слова, связанные с асинхронной обработкой (async и await). Вы увидите, что указанные средства помогают значительно упростить процесс создания отзывчивых многопоточных программных приложений.
Отношения между процессом, доменом приложения, контекстом и потоком
В главе 14 поток определялся как путь выполнения внутри исполняемого приложения. Хотя многие приложения .NET Core могут успешно и продуктивно работать, будучи однопоточными, первичный поток сборки (создаваемый исполняющей средой при выполнении точки входа приложения) в любое время может порождать вторичные потоки для выполнения дополнительных единиц работы. За счет создания дополнительных потоков можно строить более отзывчивые (но не обязательно быстрее выполняющиеся на одноядерных машинах) приложения.
Пространство имен System.Threading появилось в версии .NET 1.0 и предлагает один из подходов к построению многопоточных приложений. Равным типом в этом пространстве имен можно назвать, пожалуй, класс Thread, поскольку он представляет отдельный поток. Если необходимо программно получить ссылку на поток, который в текущий момент выполняет заданный член, то нужно просто обратиться к статическому свойству Thread.CurrentThread:
static void ExtractExecutingThread()
{
// Получить поток, который в настоящий момент выполняет данный метод.
Thread currThread = Thread.CurrentThread;
}
Вспомните, что в .NET Core существует только один домен приложения. Хотя создавать дополнительные домены приложений нельзя, домен приложения может иметь многочисленные потоки, выполняющиеся в каждый конкретный момент времени. Чтобы получить ссылку на домен приложения, который обслуживает приложение, понадобится вызвать статический метод Thread.GetDomain():
static void ExtractAppDomainHostingThread()
{
// Получить домен приложения, обслуживающий текущий поток.
AppDomain ad = Thread.GetDomain();
}
Одиночный поток в любой момент также может быть перенесен в контекст выполнения и перемещаться внутри нового контекста выполнения по прихоти среды .NET Core Runtime. Для получения текущего контекста выполнения, в котором выполняется поток, используется статическое свойство Thread.CurrentThread.ExecutionContext:
(window.adrunTag = window.adrunTag || []).push({v: 1, el: 'adrun-4-390', c: 4, b: 390})static void ExtractCurrentThreadExecutionContext()
{
// Получить контекст выполнения, в котором работает текущий поток.
ExecutionContext ctx =
Thread.CurrentThread.ExecutionContext;
}
Еще раз: за перемещение потоков в контекст выполнения и из него отвечает среда .NET Core Runtime. Как разработчик приложений .NET Core, вы всегда остаетесь в блаженном неведении относительно того, где завершается каждый конкретный поток. Тем не менее, вы должны быть осведомлены о разнообразных способах получения лежащих в основе примитивов.
Сложность, связанная с параллелизмом
Один из многих болезненных аспектов многопоточного программирования связан с ограниченным контролем над тем, как операционная система или исполняющая среда задействует потоки. Например, написав блок кода, который создает новый поток выполнения, нельзя гарантировать, что этот поток запустится немедленно. Взамен такой код только инструктирует операционную систему или исполняющую среду о необходимости как можно более скорого запуска потока (что обычно происходит, когда планировщик потоков добирается до него).
Кроме того, учитывая, что потоки могут перемещаться между границами приложений и контекстов, как требуется исполняющей среде, вы должны представлять, какие аспекты приложения являются изменчивыми в потоках (например, подвергаются многопоточному доступу), а какие операции считаются атомарными (операции, изменчивые в потоках, опасны).
Чтобы проиллюстрировать проблему, давайте предположим, что поток вызывает метод специфичного объекта. Теперь представим, что поток приостановлен планировщиком потока, чтобы позволить другому потоку обратиться к тому же методу того же самого объекта.
Если исходный поток не завершил свою операцию, тогда второй входящий поток может увидеть объект в частично модифицированном состоянии. В таком случае второй поток по существу читает фиктивные данные, что определенно может привести к очень странным (и трудно обнаруживаемым) ошибкам, которые еще труднее воспроизвести и устранить.
С другой стороны, атомарные операции в многопоточной среде всегда безопасны. К сожалению, в библиотеках базовых классов .NET Core есть лишь несколько гарантированно атомарных операций. Даже действие по присваиванию значения переменной-члену не является атомарным! Если только в документации по .NET Core специально не сказано об атомарности операции, то вы обязаны считать ее изменчивой в потоках и предпринимать соответствующие меры предосторожности.
Роль синхронизации потоков
К настоящему моменту должно быть ясно, что многопоточные программы сами по себе довольно изменчивы, т.к. многочисленные потоки могут оперировать разделяемыми ресурсами (более или менее) одновременно. Чтобы защитить ресурсы приложений от возможного повреждения, разработчики приложений .NET Core должны применять потоковые примитивы (такие как блокировки, мониторы, атрибут [Synchronization] или поддержка языковых ключевых слов) для управления доступом между выполняющимися потоками.
Несмотря на то что платформа .NET Core не способна полностью скрыть сложности, связанные с построением надежных многопоточных приложений, сам процесс был значительно упрощен. Используя типы из пространства имен System.Threading, библиотеку TPL и ключевые слова async и await языка С#, можно работать с множеством потоков, прикладывая минимальные усилия.
- Понимание SQL - Мартин Грубер - Базы данных