Конкурентность в C#. Асинхронное, параллельное и многопоточное программирование
Қосымшада ыңғайлырақҚосымшаны жүктеуге арналған QRRuStore · Samsung Galaxy Store
Huawei AppGallery · Xiaomi GetApps

автордың кітабын онлайн тегін оқу  Конкурентность в C#. Асинхронное, параллельное и многопоточное программирование

 

Стивен Клири
Конкурентность в C#. Асинхронное, параллельное и многопоточное программирование. 2-е межд. изд.
2020

Переводчик Е. Матвеев

Литературный редактор А. Руденко

Художник В. Мостипан

Корректоры Н. Сидорова, Н. Сулейманова


 

Стивен Клири

Конкурентность в C#. Асинхронное, параллельное и многопоточное программирование. 2-е межд. изд. . — СПб.: Питер, 2020.

 

ISBN 978-5-4461-1572-3

© ООО Издательство "Питер", 2020

 

Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.

 

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

Скотт Ханзельман (Scott Hanselman), главный администратор проекта, ASP.NET и Azure Web Tools, Microsoft

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

Джон Скит (Jon Skeet), старший инженер-разработчик в Google

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

Стивен Тауб (Stephen Toub), главный архитектор, Microsoft

Предисловие

Животное на обложке — мусанг, или малайская пальмовая куница, — пожалуй, отлично подходит для представления темы этой книги. Пока я не увидел обложку, я ничего не знал о нем и поэтому решил поискать информацию. Мусанги считаются вредителями, потому что засоряют своим пометом чердаки и шумят. Их анальные железы выделяют секрет с противным запахом. Мусанг относится к исчезающим видам из категории «Вызывающие наименьшее опасение», что по сути является политкорректным аналогом утверждения «Убивайте сколько угодно; никому не жалко». Мусанги поедают спелые плоды кофейного дерева (кофейные вишни), которые проходят через их желудочно-кишечный тракт.  Копи-лувак, один из самых дорогих видов кофе в мире, делается из кофейных зерен, извлеченных из испражнений мусанга. По утверждениям Американской ассоциации спешиалти кофе, «он просто имеет неприятный вкус».

Все это делает мусанга идеальным символом для конкурентной и многопоточной разработки. Для непосвященного конкурентность и многопоточность нежелательны. Из-за них добропорядочный код начинает вести себя совершенно непостижимым образом. Состояния гонки и т.д. приводят к катастрофическим сбоям (которые, похоже, всегда происходят в продакшен или во время демонстрации). Некоторые разработчики заходят настолько далеко, что заявляют: «Потоки — зло», и полностью избегают конкурентности. Немногочисленная группа разработчиков вошла во вкус и использует конкурентность без опасений; но многие в прошлом уже обжигались на ней, и от прошлого опыта у них остались неприятные воспоминания.

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

К счастью, существует множество современных библиотек, которые значительно упрощают конкурентность! Параллельная обработка и асинхронное программирование перестали быть уделом избранных. Эти библиотеки повышают уровень абстракции, вследствие чего разработка масштабируемых приложений с хорошей скоростью реакции становятся вполне реальным делом для любого разработчика. Если в прошлом, когда конкурентное программирование было исключительно сложным делом и вы на нем обожглись, рекомендую сделать новую попытку, вооружившись современным инструментарием. Мы, наверное, никогда не сможем назвать конкурентность простым делом, но сейчас она уже не так сложна, как раньше!

Для кого написана эта книга

Эта книга написана для разработчиков, которые хотят освоить современные подходы к конкурентному программированию. Предполагается, что читатель уже обладает опытом программирования .NET, включая понимание обобщенных коллекций, перечисляемых объектов и LINQ. Знание многопоточного или асинхронного программирования не потребуется. Если у вас имеется некоторый опыт в этих областях, книга все равно может вам пригодиться, потому что в ней представлены новые библиотеки — более безопасные и простые в использовании.

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

Почему я написал эту книгу

На заре своей карьеры я изучал многопоточное программирование методом проб и ошибок. Затем я изучал асинхронное программирование методом проб и ошибок. Хотя и то и другое принесло полезный опыт, я бы предпочел иметь тогда некоторые инструменты и ресурсы, которые доступны сейчас. В частности, поддержка async и await в современных языках .NET — настоящее сокровище.

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

Считаю, что такой подход неверен. В этой книге рассматриваются только современные подходы к реализации конкурентности. Это вовсе не значит, что понимание всех низкоуровневых концепций не принесет вам пользы. Когда я изучал программирование в колледже, на одном из курсов мне пришлось строить виртуальный процессор из набора элементарных логических вентилей, а на другом — изучать программирование на языке ассемблера. За всю профессиональную карьеру я не спроектировал ни одного процессора и написал всего пару десятков строк ассемблерного кода, но понимание основ приносит пользу ежедневно. И все же лучше начинать с высокоуровневых абстракций; на моем первом курсе программирования рассматривался вовсе не язык ассемблера.

Эта книга занимает определенную нишу: она содержит введение (и справочник) по конкурентности с использованием современных методов. В ней рассматриваются различные виды конкурентности, включая параллельную обработку, асинхронное и реактивное программирование. Тем не менее в ней не рассматриваются устаревшие методы, которые описаны в других книгах и сетевых ресурсах.  

Типографские соглашения

В этой книге приняты следующие типографские соглашения:

Курсив

Используется для обозначения новых терминов.

Моноширинныйшрифт

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

Так выделяются советы и предложения.

Так обозначаются предупреждения и предостережения.

Структура книги

Книга имеет следующую структуру:

• В главе 1 содержится введение в различные виды конкурентности, описанные в книге: параллелизм, асинхронное и реактивное программирование, потоки данных.

• В главах 2–6 представлено более подробное введение в разновидности конкурентности.

• В каждой из оставшихся глав рассматривается конкретный аспект конкурентности; они также могут рассматриваться как сборник решений типичных проблем.

Рекомендую прочитать (или по крайней мере просмотреть) первую главу, даже если вы уже знакомы с некоторыми разновидностями конкурентности.

На момент отправки книги в печать .NET Core 3.0 еще находится на стадии бета-тестирования, поэтому некоторые нюансы асинхронных потоков могут измениться.

Благодарности

Эта книга просто не смогла бы появиться без помощи множества людей!

Прежде всего хочу выразить благодарность своему Господу и Спасителю Иисусу Христу. Принятие христианства стало самым важным решением в моей жизни! Если вам понадобится дополнительная информация по этой теме, вы можете связаться со мной на моем сайте http://stephencleary.com/.

Также я благодарен своей семье, простившей мне все то время, которое я мог бы провести с ней.  Когда я начал работать над книгой, некоторые друзья говорили мне: «Попрощайся с семьей на следующий год!» Я думал, что они шутят. Моя жена Мэнди и наши дети Эс-Ди и Эмма проявляли понимание, когда я проводил долгие дни за работой, а потом писал по вечерам и выходным. Спасибо вам всем. Я люблю вас!

Конечно, эта книга была бы далеко не такой качественной, если бы не мои редакторы и научные редакторы: Стивен Тауб (Stephen Toub), Петр Ондерка (Petr Onderka) («svick»), Ник Палдино (Nick Paldino) («casperOne»), Ли Кэмпбелл (Lee Campbell) и Педро Феликс (Pedro Felix). И если в книгу прокрались какие-либо неточности — это целиком их вина… Шучу! Их мнение оказало неоценимую помощь в формировании (и исправлении) материала, а за все оставшиеся ошибки, конечно, отвечаю только я. Выражаю особую благодарность Стивену Таубу (Stephen Toub), который научил меня «трюку с логическим аргументом» (рецепт 14.5) и рассказал о бесчисленных нюансах, связанных с async, и Ли Кэмпбеллу (Lee Campbell), который помог мне освоить System.Reactive и сделать мой наблюдаемый код более идиоматическим.

Наконец, я хочу поблагодарить некоторых людей, от которых я узнал об этих методах: Стивена Тауба (Stephen Toub), Люциана Вищика (Lucian Wischik), Томаса Левеска (Thomas Levesque), Ли Кэмпбелла (Lee Campbell), сообщество Stack Overflow и форумов MSDN, а также участников конференций по программированию в моем родном штате Мичиган. Мне нравится быть частью сообщества разработчиков ПО, и если эта книга кому-то поможет, то стоит поблагодарить многих других, показавших правильный путь. Спасибо всем!

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

Обратите внимание: по всей книге автор дает перекрестные ссылки на рецепты. Нумерованные подзаголовки в главах и являются этими рецептами.

Глава 1. Конкурентность: общие сведения

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

Знакомство с конкурентностью

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

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

Выполнение сразу нескольких действий в одно и то же время.

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

Многие разработчики, слыша термин «конкурентность», немедленно думают о «многопоточности». Тем не менее эти два понятия следует различать.

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

Форма конкурентности, использующая несколько программных потоков выполнения.

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

Как только вы вводите команду new Thread(), все кончено: ваш проект уже содержит устаревший код.

Только не подумайте, что многопоточность мертва! Многопоточность продолжает жить в пулах потоков — полезном месте для постановки рабочих операций в очередь, которое автоматически регулируется в зависимости от нагрузки. В свою очередь, с пулом потоков становится возможной одна важная форма конкурентности: параллельная обработка.

Параллельная обработка

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

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

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

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

Разновидность конкурентности, использующая обещания или обратные вызовы для предотвращения создания лишних потоков.

Обещание (future/promise), или преднамеченный тип — тип, представляю­щий некоторую операцию, которая завершится в будущем. Примеры современных типов обещаний в .NET — Task и Task<TResult>. Более старые асинхронные API используют обратные вызовы или события вместо обещаний. В асинхронном программировании центральное место занимает идея асинхронной операции — некоторой запущенной операции, которая завершится через некоторое время. Хотя операция продолжается, она не блокирует исходный поток; поток, который запустил операцию, свободен для выполнения другой работы. Когда операция завершится, она уведомляет свое обещание или активизирует обратный вызов или событие, чтобы приложение узнало о завершении.

Асинхронное программирование — мощная разновидность конкурентности, оно до недавнего времени требовало чрезвычайно сложного кода. Благодаря поддержке async и await в современных языках асинхронное программирование становится почти таким же простым, как и синхронное (неконкурентности) программирование.

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

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

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

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

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

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

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

Оба преимущества асинхронного программирования обусловлены одним и тем же аспектом: асинхронное программирование освобождает потоки. Для GUI-программ асинхронное программирование освобождает UI-поток; это позволяет графическому приложению сохранить высокую скорость отклика на ввод пользователя. Для серверных приложений асинхронное программирование освобождает потоки запросов и позволяет серверу использовать свои потоки для обслуживания большего количества запросов.

В современных асинхронных приложениях .NET используются два ключевых слова: async  и  await. Ключевое слово async добавляется в объявление метода и имеет двойное назначение: оно разрешает использование ключевого слова await внутри этого метода и приказывает компилятору сгенерировать для этого метода конечный автомат по аналогии с тем, как работает yield return. Метод с ключевым словом async может вернуть Task<TResult>, если он возвращает значение; Task — если он не возвращает значения; или любой другой «сходный» тип — такой, как ValueTask. Кроме того, async-метод может вернуть IAsyncEnumerable<T> или IAsyncEnumerator<T>, если он возвращает несколько значений в перечислении. «Сходные» типы представляют обещания; они могут уведомлять вызывающий код о завершении async-метода.

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

С учетом всего сказанного рассмотрим короткий пример:

async Task DoSomethingAsync()

{

  int value = 13;

 

  // Асинхронно ожидать 1 секунду.

  await Task.Delay(TimeSpan.FromSeconds(1));

 

  value *= 2;

 

  // Асинхронно ожидать 1 секунду.

  await Task.Delay(TimeSpan.FromSeconds(1));

 

  Trace.WriteLine(value);

}

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

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

При выполнении await для задачи (самый распространенный сценарий) в момент, когда await решает приостановить метод, сохраняется контекст. Это текущий объект SynchronizationContext, если только он не равен null (в этом случае контекстом является текущий объект TaskScheduler). Метод возобновляет выполнение в этом сохраненном контексте. Обычно контекстом является UI-контекст (для UI-потока) или контекст пула потоков (в большинстве других ситуаций). Если вы пишете приложение ASP.NET Classic (до Core), то контекстом также может быть контекст запроса ASP.NET.  В ASP.NET Core используется контекст пула потоков вместо специального контекста запроса.

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

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

async Task DoSomethingAsync()

{

  int value = 13;

 

  // Асинхронно ожидать 1 секунду.

  await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

 

  value *= 2;

 

  // Асинхронно ожидать 1 секунду.

  await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);

 

  Trace.WriteLine(value);

}

Хорошей практикой программирования считается вызывать Configure­Await в базовых «библиотечных» методах и возобновлять контекст только тогда, когда потребуется — в ваших внешних методах «пользовательского интерфейса».

Ключевое слово await не ограничивается работой с задачами, оно может работать с любым объектом, допускающим ожидание (awaitable), построенным по определенной схеме. Например, библиотека Base Class Library включает тип ValueTask<T>, который сокращает затраты памяти, если результат в основном является синхронным; например, если результат может быть прочитан из кэша в памяти. Тип ValueTask<T> не преобразуется в Task<T> напрямую, но строится по схеме, допускающей ожидание, поэтому может использоваться с await. Также существуют другие примеры, и вы можете строить свои собственные, но в большинстве случаев await получает Task или Task<TResult>.

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

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

async Task TrySomethingAsync()

{

  try

  {

    await PossibleExceptionAsync();

  }

  catch (NotSupportedException ex)

  {

    LogException(ex);

    throw;

  }

}

Когда async-метод выдает (или распространяет) исключение, оно помещается в возвращаемый объект Task, и задача Task завершается. При выполнении await для этого объекта Task оператор await получает это исключение и (заново) выдает его так, что исходная трассировка стека сохраняется. Такой код, как в примере ниже, будет работать так, как ожидается, если PossibleExceptionAsync является async-методом:

async Task TrySomethingAsync()

{

  // Исключение попадает в Task, а не выдается напрямую.

  Task task = PossibleExceptionAsync();

 

  try

  {

    // Исключение из Task exception будет выдано здесь, в точке await.

    await task;

  }

  catch (NotSupportedException ex)

  {

    LogException(ex);

    throw;

  }

}

Относительно async-методов существует одна важная рекомендация: при использовании ключевого слова async лучше позволить ему распространяться в вашем коде. Если вы вызываете async-метод, следует (в конечном итоге) выполнить await для возвращаемой им задачи. Боритесь с искушением вызвать Task.Wait, Task<TResult>.Result или GetAwaiter().GetResult(): это приведет к взаимоблокировке (deadlock). Рассмотрим следующий метод:

async Task WaitAsync()

{

  // await сохранит текущий контекст ...

  await Task.Delay(TimeSpan.FromSeconds(1));

  // ... и попытается возобновить метод в этой точке с этим контекстом.

}

 

void Deadlock()

{

  // Начать задержку.

  Task task = WaitAsync();

  // Синхронное блокирование с ожиданием завершения async-метода.

  task.Wait();

}

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

Используйте async по полной программе.

Если вы хотите более подробно изучить async, компания Microsoft предоставляет великолепную документацию по этой теме. Рекомендую прочитать по крайней мере обзор «Asynchronous Programming» и «Task-base

...