Concurrenc in C# Cookbook / Конкурентность в C# книга рецептов: Обзор конкурентного программирования

← →
Английский оригинал Перевод на русский

Сожалеем, но текст оригинала доступен только зарегистрированным пользователям.

Конкурентность - это ключевой аспект качественной разработки. В течение десятилетий Параллелизм был возможен, но сложен в реализации. Приложения, использующие Конкурентные приложения было тяжело создавать, тяжело отлаживать и тяжело поддерживать. В результате многие разработчики выбирали более простой путь и отказывались от них. Тем не менее благодаря различным библиотекам и языковым средствам доступным современным .NET приложения конкурентность стал гораздо доступнее и проще. Когда была выпущена Visual Studio 2012 Microsoft значительно снизила уровень вхождения в параллельное программирование. Раньше программисты применяющие конкурентность, в подавляющем большинстве, были профессионалами. В настоящее время каждый разработчик может(и должен) овладеть конкурентным программированием.

masterofsea147 29.01.20 в 23:00

0

1.1.Введение в конкурентность

masterofsea147 29.01.20 в 23:00

0

Перед тем как продолжить я бы хотел внести ясность в терминологию, которую я буду использовать на протяжении всей этой книги. Начнем с конкурентности

masterofsea147 29.01.20 в 23:00

0

Конкурентность

masterofsea147 29.01.20 в 23:00

0

Одновременное выполнение нескольких задач.

masterofsea147 29.01.20 в 23:02

0

Я надеюсь, что польза конкурентности очевидна. Приложения конечного пользователя применяют ее для того, чтобы отвечать на ввод пользователя и одновременно производить запись в базу данных. Серверные приложения используют конкурентность для отправки второго запроса, пока происходит завершение первого. Вам необходима конкурентность каждый раз когда вам требуется, чтобы приложение выполняло какую-то задачу и при этом занималось чем-то еще. Практически каждое приложение в мире может извлечь пользу из конкурентности.

masterofsea147 29.01.20 в 23:03

0

Во время написания этой книги (2014) большинство разработчиков услышав термин "конкурентность" сразу же думают о многопоточности. Я бы хотел провести грань между этими определениями.

masterofsea147 29.01.20 в 23:03

0

Многопоточность

masterofsea147 29.01.20 в 23:03

0

Разновидность конкурентности при которой используется более одного потока исполнения.

masterofsea147 29.01.20 в 23:04

0

Многопоточность буквально заключается в использовании множества потоков. Как мы увидим в этой книге, многопоточность - это одна из форм конкурентности, но, безусловно, не единственная. Фактически, непосредственное применение низкоуровневых типов потоков не имеет смысла в современной разработке. Высокоуровневые абстракции гораздо эффективнее нежели олдскульные методы многопоточности. Как следствие я минимизирую объяснение устаревших технологий в этой книге. Ни в одном из рецептов многопоточности в этой книге не используются типы Thread или BackgroundWorker, они заменены на современные альтернативы.

masterofsea147 29.01.20 в 23:05

0

Как только вы напечатаете new Thread(), ваш проект сразу же начнет включать в себя legacy-код

masterofsea147 29.01.20 в 23:06

0

Но только не подумайте, что многопоточность умерла! Многопоточность живет в пуле потоков(thread pool), полезном месте для хранения очереди заданий, автоматически подстраивающемся под среду исполнения. В свою очередь, пул потоков обеспечивает еще одну важную форму конкурентности: параллельную обработку.

masterofsea147 29.01.20 в 23:07

0

Параллельная обработка(параллельные вычисления)

masterofsea147 29.01.20 в 23:07

0

Выполнение большого объема работы за счет разделения ее между несколькими потоками, которые работают одновременно.

masterofsea147 29.01.20 в 23:07

0

Параллельная обработка(или параллельное программирование) использует многопоточность для максимизации активного использования процессоров. Современные процессоры обладают несколькими ядрами и при наличии большого объема работы бессмысленно выполнять ее на одном ядре, в то время пока остальные ядра бездействуют. Параллельная обработка разделит работу между несколькими потоками, каждый из которых будет независимо выполняться на разных ядрах.

masterofsea147 29.01.20 в 23:08

0

Параллельная обработка - одна из разновидностей многопоточности и многопоточность - одна из форм конкурентности. Существует еще один тип конкурентности, который крайне важен в современных приложениях но (в настоящее время) не знаком многим разработчикам: асинхронное программирование.

masterofsea147 29.01.20 в 23:09

0

Асинхронное программирование

masterofsea147 29.01.20 в 23:09

0

Форма конкурентности, использующая продолжения или коллбэки, чтобы избежать блокировки потоков.

masterofsea147 29.01.20 в 23:10

0

Продолжение - это тип представляющий собой некоторую операцию, которая будет выполнена в будущем. Современные типы для продолжений в .NET - Task и Task<TResult>. Старые асинхронные API используют коллбэки или события вместо продолжений. Асинхронное программирование сосредоточено вокруг идеи асинхронных операций - операций, которые запускаются и завершаются через некоторое время. Во время выполнения асинхронной операции поток не блокируется, вместо этого поток который запускает операцию освобождается для других задач.
Когда операция завершается, она уведомляет о своем продолжении или вызывает событие обратного вызова, чтобы приложение узнало, что операция завершена.

masterofsea147 29.01.20 в 23:16

0
+1

Асинхронное программирование - это мощная форма конкурентности, однако до недавнего времени она требовала крайне сложной реализации. Async и await, поддерживаемый в VS2012 делают асинхронное программирование практически таким же простым как и синхронное(неконкурентное) программирование.

masterofsea147 29.01.20 в 23:14

0

Другая форма конкурентности - это реактивное программирование. Асинхронное программирование подразумевает что приложение начнет выполнение операции, которая будет выполнена единожды позднее. Реактивное программирование тесно связано с асинхронных программированием, но основано на асинхронных событиях вместо асинхронных операций. Асинхронные события могут не иметь фактического «начала», могут происходить в любое время и могут вызываться многократно. Одним из примеров является пользовательский ввод.

masterofsea147 29.01.20 в 23:17

0

Реактивное программирование

masterofsea147 29.01.20 в 23:17

0

Декларативный стиль программирования при котором приложение реагирует на события.

masterofsea147 29.01.20 в 23:17

0

Если вы представите приложение в виде массивного конечного автомата, поведение приложения можно будет описать как реакцию на последовательность событий в виде обновления его состояния при каждом событии. Это не столь абстрактно как вам может показаться, современные фреймворки делают этот подход весьма полезным в реальных приложениях. Реактивные приложения не обязательно являются конкурентными, но они тесно связаны с конкурентностью, поэтому мы рассмотрим их основы в этой книге.

masterofsea147 29.01.20 в 23:18

0

Обычно в конкурентных приложениях используется сочетание этих техник. Большинство приложений используют хотя бы многопоточное (в виде пулла потоков) и асинхронное программирование. Не бойтесь смешивать и сочетать различные формы конкурентности, используя соответствующий инструмент для каждой части приложения.

masterofsea147 29.01.20 в 23:18

0

1.2. Введение в асинхронное программирование

masterofsea147 29.01.20 в 23:19

0

Асинхронное программирование имеет два основных преимущества. Первое преимущество для GUI-приложений конечного пользователя: асинхронное программирование обеспечивает отзывчивость. Все мы имели дело с программами, которые временно блокируются, во время выполнения продолжительных операций; асинхронная программа может оставаться чувствительной к пользовательскому вводу во время таких операций. Второе преимущество для серверных приложений: асинхронное программирование обеспечивает масштабируемость.

masterofsea147 29.01.20 в 23:25

0

Современные асинхронные .NET-приложения используют два ключевых слова: async и await. Ключевое слово async входит в объявление метода, и его основная цель состоит в том, чтобы предоставить возможность использования ключевого слова await в этом методе (ключевые слова были введены в виде пары по причинам обратной совместимости). Асинхронный метод должен иметь возвращаемый тип Task <T>, если он возвращает результат, или Task, в обратном случае. Эти типы представляют собой продолжения; они уведомляют вызывающий код о завершении асинхронного метода.

masterofsea147 29.01.20 в 23:29

0

Не используйте async void! Создать async метод с возвращаемым типов void возможно, но вы должны делать это только в том случае, когда вы создаете асинхронный обработчик событий. Обычный async метод, не возвращающий значения, должен иметь возвращаемый тип Task, а не void.

masterofsea147 29.01.20 в 23:19

0

Давайте рассмотрим пример: листинг1

masterofsea147 29.01.20 в 23:31

0

Асинхронный метод начинает выполняться синхронно, как и любой другой метод. Далее благодаря оператору await происходит асинхронное ожидание его аргумента. Сначала происходит проверка, завершена ли операция, являющаяся операндом await; если это так, он продолжает выполняться (синхронно). В противном случае он приостановит асинхронный метод и вернет незавершенную задачу. Когда эта операция завершится, через некоторое время, асинхронный метод возобновит выполнение.

masterofsea147 29.01.20 в 23:35

0

Вы можете представить асинхронный метод как последовательность частей кода, выполняющихся синхронно и разделенных операторами await. Первая синхронная часть кода выполняется в потоке, вызывающем метод, но где выполняются другие синхронные части? Ответ на это вопрос непростой.

masterofsea147 29.01.20 в 23:41

0

<Когда вы ожидаете задачу (наиболее распространенный сценарий), контекст захватывается, когда оператор await решает приостановить метод. Этот контекст является текущим контекстом SynchronizationContext, если он не равен null, и в этом случае контекст является текущим TaskScheduler.> Метод возобновляет выполнение в этом захваченном контексте. Обычно этим контекстом является контекст пользовательского интерфейса (если вы находитесь в потоке GUI), контекст запроса ASP.NET (если вы обрабатываете запрос ASP.NET) или контекст пула потоков (в большинстве других ситуаций).

masterofsea147 29.01.20 в 23:47

0
+1

Таким образом, в предыдущем коде все синхронные части будут пытаться возобновить исходный контекст. Если вы вызываете DoSomethingAsync из потока пользовательского интерфейса, каждая из его синхронных частей будет работать в потоке пользовательского интерфейса; но если вы вызываете его из пула, каждая из его синхронных частей будет выполняться в пуле потоков.

masterofsea147 29.01.20 в 23:52

0

Вы можете избежать этого поведения по умолчанию, с помощью метода расширения ConfigureAwait если передадите ему в качестве параметра continueOnCapturedContext - false. Следующий код будет запущен в вызывающем потоке, и после его приостановки оператором await он возобновится в пуле потоков. <листинг2>

masterofsea147 29.01.20 в 23:57

0

Рекомендуется всегда вызывать ConfigureAwait в ваших основных «библиотечных» методах и возобновлять контекст только тогда, когда вам это действительно нужно - в ваших методах «пользовательского интерфейса».

masterofsea147 30.01.20 в 00:04

0

Оператор await не ограничивается работой с задачами; он может работать с любым видом ожидания, который удовлетворяет определенному шаблону. Например, Windows Runtime API определяет свои собственные интерфейсы для асинхронных операций. Они не могут быть преобразованы в Task, но они следуют шаблону ожидания, поэтому вы можете непосредственно ожидать их. Эти виды ожиданий более распространены в приложениях Windows Store, но в большинстве случаев оператор await будем иметь дело с Task или Task<T>.

masterofsea147 30.01.20 в 00:11

0

Существует два основных способа создания экземпляра Task. Некоторые задачи представляют собой реальный код, который должен выполнять ЦП; эти вычислительные задачи должны быть созданы путем вызова метода Task.Run (или TaskFactory.StartNew, <если вам нужно, чтобы они выполнялись в определенном планировщике>). Другие задачи представляют собой уведомления; эти основанные на событиях задачи создаются с помощью TaskCompletionSource <T>. Большинство задач ввода / вывода используют TaskCompletionSource <T>.

masterofsea147 30.01.20 в 00:23

0
+1

Обработка ошибок - обычное дело при использовании async и await. В следующем фрагменте кода PossibleExceptionAsync может вызвать исключение NotSupportedException, но TrySomethingAsync может перехватить исключение естественным образом. Пойманное исключение имеет правильно сохраненную трассировку стека и не является обернутым в TargetInvocationException или AggregateException: <list3>

masterofsea147 30.01.20 в 00:31

0

Когда асинхронный метод генерирует исключение, исключение помещается в возвращаемую задачу, и она завершается. Когда эта задача ожидается, оператор await извлекает это исключение и выбрасывает его таким образом, чтобы сохранить исходную трассировку стека. <Таким образом, подобный код работал бы так, как ожидалось, если бы OptionExceptionAsync был асинхронным методом:> <listing4>

masterofsea147 30.01.20 в 00:39

0
+1

В отношении асинхронных методов есть еще одна важная рекомендация: как только вы начнете использовать async, лучше всего позволить ему распространиться в вашем коде. Если вы вызываете асинхронный метод, вы должны (в конце концов) дождаться выполнения задачи, которую он возвращает. Не поддавайтесь искушению вызова Task.Wait или Task<T>.Result; это может стать причиной дедлока. Рассмотрим этот метод: <listing5>

masterofsea147 30.01.20 в 00:42

0

Этот код приводит к дедлоку, если вызывается из контекста пользовательского интерфейса или контекста ASP.NET. Это связано с тем, что оба эти контекста допускают только один поток. Deadlock вызовет WaitAsync, с которого начинается задержка. Затем Deadlock(синхронно) ожидает завершения этого метода, блокируя поток контекста. Когда задержка завершается, await пытается возобновить WaitAsync в захваченном контексте, но не может, потому что в контексте уже есть поток, заблокированный в контексте, и контекст допускает только один поток за раз. Взаимная блокировка может быть предотвращена двумя способами: вы можете использовать ConfigureAwait(false) в WaitAsync (что приводит к игнорированию его контекста), или вы можете установить ожидание вызова WaitAsync (превратив Deadlock в асинхронный метод).

masterofsea147 30.01.20 в 00:52

0

Если вы используете async - используйте его всегда.

masterofsea147 30.01.20 в 14:12

0

1.3.Введение в параллельное программирование

masterofsea147 30.01.20 в 14:12

0

Параллельное программирование должно использоваться всякий раз, когда у вас есть значительное количество вычислительной работы, которая может быть разделена на независимые части. Параллельное программирование временно увеличивает нагрузку на процессор для повышения пропускной способности; это отлично подходит для клиентских систем, где процессоры часто простаивают, но редко подходят для серверов. Большинство серверных систем имеют встроенный параллелизм; например, ASP.NET обрабатывает несколько запросов параллельно. Написание параллельного кода для серверного приложения, однако,может быть полезно в некоторых ситуациях (если вы заранее знаете, что число пользователей одновременно работающих с сервером будет небольшим), но в большинстве других случаев параллелизм для сервера не принесет какой-либо практической выгоды.

masterofsea147 30.01.20 в 14:38

0

Существует две формы параллелизма: параллелизм данных и параллелизм задач. Параллелизм данных - это параллелизм, при котором у вас имеется множество фрагментов данных для обработки, и обработка каждого фрагмента производится независимо от остальных данных. Параллелизм задач - это параллелизм, при котором у вас есть пул работы, и каждая часть работы в основном независима от других частей. Параллелизм задач может быть динамическим - если одна часть работы приводит к нескольким дополнительным частям, они могут быть добавлены в пул работы.

masterofsea147 30.01.20 в 14:49

0

Есть несколько способов реализации параллелизма данных. Метод Parallel.ForEach похож на цикл foreach и должен использоваться по возможности. Parallel.ForEach рассматривается в рецепте 3.1. Класс Parallel также предоставляет метод Parallel.For, который похож на цикл for и может использоваться, если обработка данных зависит от индексации. Код с использованием Parallel.ForEach выглядит следующим образом: <list6>

masterofsea147 30.01.20 в 15:05

0

Другой вариант - PLINQ (Parallel LINQ), который предоставляет метод расширения AsParallel для запросов LINQ. Parallel является более дружественным к ресурсам, чем PLINQ; Parallel будет учитывать взаимодействие с другими процессами в системе, в то время как PLINQ (по умолчанию) будет вести себя агрессивно, пытаясь занять собой все доступные процессоры. Однако, PLINQ имеет более элегантный код. Применение PLINQ показано в рецепте 3.5: <listing7>

masterofsea147 30.01.20 в 15:21

0

Независимо от выбранного вами метода, при параллельной обработке данных необходимо следовать единому правилу.

masterofsea147 30.01.20 в 15:30

0

Параллельно обрабатываемые фрагменты должны быть максимально независимыми друг от друга.

masterofsea147 30.01.20 в 15:31

0
Минутку...

Следующая страница →