Concurrenc in C# Cookbook / Конкурентность в C# книга рецептов: Обзор конкурентного программирования
| Английский оригинал | Перевод на русский | |
|---|---|---|
Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Конкурентность - это ключевой аспект качественной разработки. В течение десятилетий Параллелизм был возможен, но сложен в реализации. Приложения, использующие Конкурентные приложения было тяжело создавать, тяжело отлаживать и тяжело поддерживать. В результате многие разработчики выбирали более простой путь и отказывались от них. Тем не менее благодаря различным библиотекам и языковым средствам доступным современным .NET приложения конкурентность стал гораздо доступнее и проще. Когда была выпущена Visual Studio 2012 Microsoft значительно снизила уровень вхождения в параллельное программирование. Раньше программисты применяющие конкурентность, в подавляющем большинстве, были профессионалами. В настоящее время каждый разработчик может(и должен) овладеть конкурентным программированием. masterofsea147 29.01.20 в 23:00 |
|
|
1.1.Введение в конкурентность masterofsea147 29.01.20 в 23:00 |
||
|
Перед тем как продолжить я бы хотел внести ясность в терминологию, которую я буду использовать на протяжении всей этой книги. Начнем с конкурентности masterofsea147 29.01.20 в 23:00 |
||
|
Конкурентность masterofsea147 29.01.20 в 23:00 |
||
|
Одновременное выполнение нескольких задач. masterofsea147 29.01.20 в 23:02 |
||
|
Я надеюсь, что польза конкурентности очевидна. Приложения конечного пользователя применяют ее для того, чтобы отвечать на ввод пользователя и одновременно производить запись в базу данных. Серверные приложения используют конкурентность для отправки второго запроса, пока происходит завершение первого. Вам необходима конкурентность каждый раз когда вам требуется, чтобы приложение выполняло какую-то задачу и при этом занималось чем-то еще. Практически каждое приложение в мире может извлечь пользу из конкурентности. masterofsea147 29.01.20 в 23:03 |
||
|
Во время написания этой книги (2014) большинство разработчиков услышав термин "конкурентность" сразу же думают о многопоточности. Я бы хотел провести грань между этими определениями. masterofsea147 29.01.20 в 23:03 |
||
|
Многопоточность masterofsea147 29.01.20 в 23:03 |
||
|
Разновидность конкурентности при которой используется более одного потока исполнения. masterofsea147 29.01.20 в 23:04 |
||
|
Многопоточность буквально заключается в использовании множества потоков. Как мы увидим в этой книге, многопоточность - это одна из форм конкурентности, но, безусловно, не единственная. Фактически, непосредственное применение низкоуровневых типов потоков не имеет смысла в современной разработке. Высокоуровневые абстракции гораздо эффективнее нежели олдскульные методы многопоточности. Как следствие я минимизирую объяснение устаревших технологий в этой книге. Ни в одном из рецептов многопоточности в этой книге не используются типы Thread или BackgroundWorker, они заменены на современные альтернативы. masterofsea147 29.01.20 в 23:05 |
||
|
Как только вы напечатаете new Thread(), ваш проект сразу же начнет включать в себя legacy-код masterofsea147 29.01.20 в 23:06 |
||
|
Но только не подумайте, что многопоточность умерла! Многопоточность живет в пуле потоков(thread pool), полезном месте для хранения очереди заданий, автоматически подстраивающемся под среду исполнения. В свою очередь, пул потоков обеспечивает еще одну важную форму конкурентности: параллельную обработку. masterofsea147 29.01.20 в 23:07 |
||
|
Параллельная обработка(параллельные вычисления) masterofsea147 29.01.20 в 23:07 |
||
|
Выполнение большого объема работы за счет разделения ее между несколькими потоками, которые работают одновременно. masterofsea147 29.01.20 в 23:07 |
||
|
Параллельная обработка(или параллельное программирование) использует многопоточность для максимизации активного использования процессоров. Современные процессоры обладают несколькими ядрами и при наличии большого объема работы бессмысленно выполнять ее на одном ядре, в то время пока остальные ядра бездействуют. Параллельная обработка разделит работу между несколькими потоками, каждый из которых будет независимо выполняться на разных ядрах. masterofsea147 29.01.20 в 23:08 |
||
|
Параллельная обработка - одна из разновидностей многопоточности и многопоточность - одна из форм конкурентности. Существует еще один тип конкурентности, который крайне важен в современных приложениях но (в настоящее время) не знаком многим разработчикам: асинхронное программирование. masterofsea147 29.01.20 в 23:09 |
||
|
Асинхронное программирование masterofsea147 29.01.20 в 23:09 |
||
|
Форма конкурентности, использующая продолжения или коллбэки, чтобы избежать блокировки потоков. masterofsea147 29.01.20 в 23:10 |
||
|
Продолжение - это тип представляющий собой некоторую операцию, которая будет выполнена в будущем. Современные типы для продолжений в .NET - Task и Task<TResult>. Старые асинхронные API используют коллбэки или события вместо продолжений. Асинхронное программирование сосредоточено вокруг идеи асинхронных операций - операций, которые запускаются и завершаются через некоторое время. Во время выполнения асинхронной операции поток не блокируется, вместо этого поток который запускает операцию освобождается для других задач. masterofsea147 29.01.20 в 23:16 |
+1 | |
|
Асинхронное программирование - это мощная форма конкурентности, однако до недавнего времени она требовала крайне сложной реализации. Async и await, поддерживаемый в VS2012 делают асинхронное программирование практически таким же простым как и синхронное(неконкурентное) программирование. masterofsea147 29.01.20 в 23:14 |
||
|
Другая форма конкурентности - это реактивное программирование. Асинхронное программирование подразумевает что приложение начнет выполнение операции, которая будет выполнена единожды позднее. Реактивное программирование тесно связано с асинхронных программированием, но основано на асинхронных событиях вместо асинхронных операций. Асинхронные события могут не иметь фактического «начала», могут происходить в любое время и могут вызываться многократно. Одним из примеров является пользовательский ввод. masterofsea147 29.01.20 в 23:17 |
||
|
Реактивное программирование masterofsea147 29.01.20 в 23:17 |
||
|
Декларативный стиль программирования при котором приложение реагирует на события. masterofsea147 29.01.20 в 23:17 |
||
|
Если вы представите приложение в виде массивного конечного автомата, поведение приложения можно будет описать как реакцию на последовательность событий в виде обновления его состояния при каждом событии. Это не столь абстрактно как вам может показаться, современные фреймворки делают этот подход весьма полезным в реальных приложениях. Реактивные приложения не обязательно являются конкурентными, но они тесно связаны с конкурентностью, поэтому мы рассмотрим их основы в этой книге. masterofsea147 29.01.20 в 23:18 |
||
|
Обычно в конкурентных приложениях используется сочетание этих техник. Большинство приложений используют хотя бы многопоточное (в виде пулла потоков) и асинхронное программирование. Не бойтесь смешивать и сочетать различные формы конкурентности, используя соответствующий инструмент для каждой части приложения. masterofsea147 29.01.20 в 23:18 |
||
|
1.2. Введение в асинхронное программирование masterofsea147 29.01.20 в 23:19 |
||
|
Асинхронное программирование имеет два основных преимущества. Первое преимущество для GUI-приложений конечного пользователя: асинхронное программирование обеспечивает отзывчивость. Все мы имели дело с программами, которые временно блокируются, во время выполнения продолжительных операций; асинхронная программа может оставаться чувствительной к пользовательскому вводу во время таких операций. Второе преимущество для серверных приложений: асинхронное программирование обеспечивает масштабируемость. masterofsea147 29.01.20 в 23:25 |
||
|
Современные асинхронные .NET-приложения используют два ключевых слова: async и await. Ключевое слово async входит в объявление метода, и его основная цель состоит в том, чтобы предоставить возможность использования ключевого слова await в этом методе (ключевые слова были введены в виде пары по причинам обратной совместимости). Асинхронный метод должен иметь возвращаемый тип Task <T>, если он возвращает результат, или Task, в обратном случае. Эти типы представляют собой продолжения; они уведомляют вызывающий код о завершении асинхронного метода. masterofsea147 29.01.20 в 23:29 |
||
|
Не используйте async void! Создать async метод с возвращаемым типов void возможно, но вы должны делать это только в том случае, когда вы создаете асинхронный обработчик событий. Обычный async метод, не возвращающий значения, должен иметь возвращаемый тип Task, а не void. masterofsea147 29.01.20 в 23:19 |
||
|
Давайте рассмотрим пример: листинг1 masterofsea147 29.01.20 в 23:31 |
||
|
Асинхронный метод начинает выполняться синхронно, как и любой другой метод. Далее благодаря оператору await происходит асинхронное ожидание его аргумента. Сначала происходит проверка, завершена ли операция, являющаяся операндом await; если это так, он продолжает выполняться (синхронно). В противном случае он приостановит асинхронный метод и вернет незавершенную задачу. Когда эта операция завершится, через некоторое время, асинхронный метод возобновит выполнение. masterofsea147 29.01.20 в 23:35 |
||
|
Вы можете представить асинхронный метод как последовательность частей кода, выполняющихся синхронно и разделенных операторами await. Первая синхронная часть кода выполняется в потоке, вызывающем метод, но где выполняются другие синхронные части? Ответ на это вопрос непростой. masterofsea147 29.01.20 в 23:41 |
||
|
<Когда вы ожидаете задачу (наиболее распространенный сценарий), контекст захватывается, когда оператор await решает приостановить метод. Этот контекст является текущим контекстом SynchronizationContext, если он не равен null, и в этом случае контекст является текущим TaskScheduler.> Метод возобновляет выполнение в этом захваченном контексте. Обычно этим контекстом является контекст пользовательского интерфейса (если вы находитесь в потоке GUI), контекст запроса ASP.NET (если вы обрабатываете запрос ASP.NET) или контекст пула потоков (в большинстве других ситуаций). masterofsea147 29.01.20 в 23:47 |
+1 | |
|
Таким образом, в предыдущем коде все синхронные части будут пытаться возобновить исходный контекст. Если вы вызываете DoSomethingAsync из потока пользовательского интерфейса, каждая из его синхронных частей будет работать в потоке пользовательского интерфейса; но если вы вызываете его из пула, каждая из его синхронных частей будет выполняться в пуле потоков. masterofsea147 29.01.20 в 23:52 |
||
|
Вы можете избежать этого поведения по умолчанию, с помощью метода расширения ConfigureAwait если передадите ему в качестве параметра continueOnCapturedContext - false. Следующий код будет запущен в вызывающем потоке, и после его приостановки оператором await он возобновится в пуле потоков. <листинг2> masterofsea147 29.01.20 в 23:57 |
||
|
Рекомендуется всегда вызывать ConfigureAwait в ваших основных «библиотечных» методах и возобновлять контекст только тогда, когда вам это действительно нужно - в ваших методах «пользовательского интерфейса». masterofsea147 30.01.20 в 00:04 |
||
|
Оператор await не ограничивается работой с задачами; он может работать с любым видом ожидания, который удовлетворяет определенному шаблону. Например, Windows Runtime API определяет свои собственные интерфейсы для асинхронных операций. Они не могут быть преобразованы в Task, но они следуют шаблону ожидания, поэтому вы можете непосредственно ожидать их. Эти виды ожиданий более распространены в приложениях Windows Store, но в большинстве случаев оператор await будем иметь дело с Task или Task<T>. masterofsea147 30.01.20 в 00:11 |
||
|
Существует два основных способа создания экземпляра Task. Некоторые задачи представляют собой реальный код, который должен выполнять ЦП; эти вычислительные задачи должны быть созданы путем вызова метода Task.Run (или TaskFactory.StartNew, <если вам нужно, чтобы они выполнялись в определенном планировщике>). Другие задачи представляют собой уведомления; эти основанные на событиях задачи создаются с помощью TaskCompletionSource <T>. Большинство задач ввода / вывода используют TaskCompletionSource <T>. masterofsea147 30.01.20 в 00:23 |
+1 | |
|
Обработка ошибок - обычное дело при использовании async и await. В следующем фрагменте кода PossibleExceptionAsync может вызвать исключение NotSupportedException, но TrySomethingAsync может перехватить исключение естественным образом. Пойманное исключение имеет правильно сохраненную трассировку стека и не является обернутым в TargetInvocationException или AggregateException: <list3> masterofsea147 30.01.20 в 00:31 |
||
|
Когда асинхронный метод генерирует исключение, исключение помещается в возвращаемую задачу, и она завершается. Когда эта задача ожидается, оператор await извлекает это исключение и выбрасывает его таким образом, чтобы сохранить исходную трассировку стека. <Таким образом, подобный код работал бы так, как ожидалось, если бы OptionExceptionAsync был асинхронным методом:> <listing4> masterofsea147 30.01.20 в 00:39 |
+1 | |
|
В отношении асинхронных методов есть еще одна важная рекомендация: как только вы начнете использовать async, лучше всего позволить ему распространиться в вашем коде. Если вы вызываете асинхронный метод, вы должны (в конце концов) дождаться выполнения задачи, которую он возвращает. Не поддавайтесь искушению вызова Task.Wait или Task<T>.Result; это может стать причиной дедлока. Рассмотрим этот метод: <listing5> masterofsea147 30.01.20 в 00:42 |
||
|
Этот код приводит к дедлоку, если вызывается из контекста пользовательского интерфейса или контекста ASP.NET. Это связано с тем, что оба эти контекста допускают только один поток. Deadlock вызовет WaitAsync, с которого начинается задержка. Затем Deadlock(синхронно) ожидает завершения этого метода, блокируя поток контекста. Когда задержка завершается, await пытается возобновить WaitAsync в захваченном контексте, но не может, потому что в контексте уже есть поток, заблокированный в контексте, и контекст допускает только один поток за раз. Взаимная блокировка может быть предотвращена двумя способами: вы можете использовать ConfigureAwait(false) в WaitAsync (что приводит к игнорированию его контекста), или вы можете установить ожидание вызова WaitAsync (превратив Deadlock в асинхронный метод). masterofsea147 30.01.20 в 00:52 |
||
|
Если вы используете async - используйте его всегда. masterofsea147 30.01.20 в 14:12 |
||
|
1.3.Введение в параллельное программирование masterofsea147 30.01.20 в 14:12 |
||
|
Параллельное программирование должно использоваться всякий раз, когда у вас есть значительное количество вычислительной работы, которая может быть разделена на независимые части. Параллельное программирование временно увеличивает нагрузку на процессор для повышения пропускной способности; это отлично подходит для клиентских систем, где процессоры часто простаивают, но редко подходят для серверов. Большинство серверных систем имеют встроенный параллелизм; например, ASP.NET обрабатывает несколько запросов параллельно. Написание параллельного кода для серверного приложения, однако,может быть полезно в некоторых ситуациях (если вы заранее знаете, что число пользователей одновременно работающих с сервером будет небольшим), но в большинстве других случаев параллелизм для сервера не принесет какой-либо практической выгоды. masterofsea147 30.01.20 в 14:38 |
||
|
Существует две формы параллелизма: параллелизм данных и параллелизм задач. Параллелизм данных - это параллелизм, при котором у вас имеется множество фрагментов данных для обработки, и обработка каждого фрагмента производится независимо от остальных данных. Параллелизм задач - это параллелизм, при котором у вас есть пул работы, и каждая часть работы в основном независима от других частей. Параллелизм задач может быть динамическим - если одна часть работы приводит к нескольким дополнительным частям, они могут быть добавлены в пул работы. masterofsea147 30.01.20 в 14:49 |
||
|
Есть несколько способов реализации параллелизма данных. Метод Parallel.ForEach похож на цикл foreach и должен использоваться по возможности. Parallel.ForEach рассматривается в рецепте 3.1. Класс Parallel также предоставляет метод Parallel.For, который похож на цикл for и может использоваться, если обработка данных зависит от индексации. Код с использованием Parallel.ForEach выглядит следующим образом: <list6> masterofsea147 30.01.20 в 15:05 |
||
|
Другой вариант - PLINQ (Parallel LINQ), который предоставляет метод расширения AsParallel для запросов LINQ. Parallel является более дружественным к ресурсам, чем PLINQ; Parallel будет учитывать взаимодействие с другими процессами в системе, в то время как PLINQ (по умолчанию) будет вести себя агрессивно, пытаясь занять собой все доступные процессоры. Однако, PLINQ имеет более элегантный код. Применение PLINQ показано в рецепте 3.5: <listing7> masterofsea147 30.01.20 в 15:21 |
||
|
Независимо от выбранного вами метода, при параллельной обработке данных необходимо следовать единому правилу. masterofsea147 30.01.20 в 15:30 |
||
|
Параллельно обрабатываемые фрагменты должны быть максимально независимыми друг от друга. masterofsea147 30.01.20 в 15:31 |
