Файл: Асинхронноепрограммирование.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 10.11.2023

Просмотров: 135

Скачиваний: 3

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.


ГЛАВА 1.
Введение
Начнем с общего введения в средства асинхронного программирова- ния (или просто async) в C# 5.0.
Асинхронное программирование
Код называется асинхронным, если он запускает какую-то длитель- ную операцию, но не дожидается ее завершения. Противоположнос- тью является блокирующий код, который ничего не делает, пока опе- рация не завершится.
К числу таких длительных операций можно отнести:
• сетевые запросы;
• доступ к диску;
• продолжительные задержки.
Основное различие заключается в том, в каком потоке выполняет- ся код. Во всех популярных языках программирования код работает в контексте какого-то потока операционной системы. Если этот поток продолжает делать что-то еще, пока выполняется длительная опера- ция, то код асинхронный. Если поток в это время ничего не делает, значит, он заблокирован и, следовательно, вы написали блокирую- щий код.
Разумеется, есть и третья стратегия ожидания результата дли- тельной операции – опрос. В этом случае вы периодически инте- ресуетесь, завершилась ли операция. Для очень коротких опе- раций такой способ иногда применяется, но в общем случае эта идея неудачна.
Вполне возможно, что вы уже применяли асинхронный код в сво- их программах. Всякий раз, запуская новый поток или пользуясь классом
ThreadPool
, вы пишете асинхронную программу, потому что текущий поток может продолжать заниматься другими вещами.
Если вам доводилось создавать веб-страницы, из которых пользова-

14
Глава 1. Введение тель может обращаться к другим страницам, то такой код был асин- хронным, потому, что внутри веб-сервера нет потока, ожидающего, когда пользователь закончит ввод данных. Кажется очевидным, но вспомните о консольном приложении, которое запрашивает данные от пользователя с помощью метода
Console.ReadLine()
, и вам ста- нет понятно, как мог бы выглядеть альтернативный блокирующий дизайн веб-приложений. Да, такой дизайн был бы кошмаром, но всё же он возможен.
Для асинхронного кода характерна типичная трудность: как узнать, когда операция завершилась? Ведь только после этого можно присту- пить к обработке ее результатов. В блокирующем коде все тривиаль- но – следующая строка помещается сразу после вызова длительной операции. Но в асинхронном мире так сделать нельзя, потому что размещенная в этом месте строка почти наверняка будет выполнена раньше, чем асинхронная операция завершится.
Для решения этой проблемы придуман целый ряд приемов, позво- ляющих выполнить код по завершении фоновой операции:
• включить нужный код в состав самой операции, после кода, составляющего ее основное назначение;
• подписаться на событие, генерируемое по завершении;
• передать делегат или лямбда-функцию, которая должна быть выполнена по завершении (обратный вызов).
Если код, следующий за асинхронной операцией, необходимо выполнить в конкретном потоке (например, в потоке пользова- тельского интерфейса в программе на базе WinForms или WPF), то приходится ставить операцию в очередь этого потока. Всё это очень утомительно.
Чем так хорош асинхронный код?
Асинхронный код освобождает поток, из которого был запущен. И это очень хорошо по многим причинам. Прежде всего, потоки потреб- ляют ресурсы компьютера, а чем меньше расходуется ресурсов, тем лучше. Часто существует лишь один поток, способный выполнить определенную задачу (например, поток пользовательского интер- фейса) и, если не освободить его быстро, то приложение перестанет реагировать на действия пользователя. Мы еще вернемся к этой теме в следующей главе.
Но самым важным мне представляется тот факт, что асинхронное выполнение открывает возможность для параллельных вычислений.


15
Что делает async?
Вы можете структурировать программу по-новому, реализовав мел- коструктурный параллелизм, но не жертвуя простотой и удобством сопровождения. Этот вопрос мы будем исследовать в главе 10.
Что такое async?
В версии C# 5.0 Microsoft добавила механизм, предстающий в виде двух новых ключевых слов: async и await
Этот механизм опирается на ряд нововведений в .NET Frame- work 4.5, без которых был бы бесполезен.
Механизм async встроен в компилятор и без поддержки с его стороны не мог бы быть реализован в библиотеке. Компилятор преобразовывает исходный код, то есть действует примерно по тому же принципу, что в случае лямбда-выражений и итераторов в предыдущих версиях C#.
Эта возможность существенно упрощает асинхронное программи- рование, избавляя от необходимости использовать сложные приемы, как то было в предыдущих версиях языка. С ее помощью можно на- писать всю программу целиком в асинхронном стиле.
В этой книгу я буду называть словом асинхронный общий стиль программирования, упростившийся после появления в C# механизма
async. На C# всегда можно было писать асинхронные программы, но это требовало значительных усилий со стороны программиста.
Что делает async?
Механизм async дает простой способ выразить, что должна делать программа по завершении длительной асинхронной операции. Ме- тод, помеченный ключевым словом async
, компилятор преобразует так, что асинхронный код выглядит очень похоже на блокирующий эквивалент. Ниже приведен простой пример блокирующего метода для загрузки веб-страницы.
private void DumpWebPage(string uri)
{
WebClient webClient = new WebClient();
string page = webClient.DownloadString(uri);
Console.WriteLine(page);
}
А вот эквивалентный ему асинхронный метод.

16
Глава 1. Введение private async void DumpWebPageAsync(string uri)
{
WebClient webClient = new WebClient();
string page = await webClient.DownloadStringTaskAsync(uri);
Console.WriteLine(page);
}
Похожи, не правда ли? Но под капотом они сильно отличаются.
Второй метод помечен ключевым словом async
. Это обязательное условие для всех методов, в которых используется ключевое слово await
. Еще мы добавили к имени метода суффикс
Async
, чтобы соб- люсти общепринятое соглашение.
Наибольший интерес представляет ключевое слово await
. Видя его, компилятор переписывает метод. Точная процедура довольно сложна, поэтому пока я приведу лишь ее не вполне корректное опи- сание, которое, на мой взгляд, полезно для понимания простых слу- чаев.
1. Весь код после await переносится в отдельный метод.
2. Новый вариант метода
DownloadString называется
Downlo- adStringTaskAsync
. Он делает то же самое, что исходный, но асинхронно.
3. Это означает, что мы можем передать ему сгенерированный метод, который будет вызываться по завершении операции.
Делается это с помощью некоторых магических манипуляций, о которых я расскажу ниже.
4. Когда загрузка страницы завершится, будет вызван наш код, которому передается загруженная строка string
; в данном случае мы просто выводим ее на консоль.
private void DumpWebPageAsync(string uri)
{
WebClient webClient = new WebClient();
webClient.DownloadStringTaskAsync(uri) <- magic(SecondHalf);
}
private void SecondHalf(string awaitedResult)
{
string page = awaitedResult;
Console.WriteLine(page);
}
Что происходит в вызывающем потоке, когда он исполняет такой код? По достижении вызова
DownloadStringTaskAsync начинается загрузка страницы. Но не в текущем потоке. В нем вызванный метод


17
сразу возвращает управление. Что делать дальше, решает вызываю- щая программа. К примеру, поток пользовательского интерфейса мог бы продолжить обработку действий пользователя. Или просто завер- шиться, освободив ресурсы. В любом случае мы написали асинхрон- ный код!
Async не решает все проблемы
Механизм async намеренно спроектирован так, чтобы максимально напоминать блокирующий код. Мы можем рассматривать длитель- ные или удаленные операции, как будто они выполняются локально и быстро, увеличив производительность за счет асинхронности.
Однако он не дает вам совсем забыть о том, что на самом деле опе- рация выполняется в фоновом режиме и происходит обратный вызов.
Необходимо иметь в виду, что многие средства языка в асинхронном режиме ведут себя по-другому, частности:
• исключения и блоки try-catch-finally;
• возвращаемые методами значения;
• потоки и контекст;
• производительность.
Не зная, что происходит в действительности, вы не сможете ни по- нять смысл неожиданных сообщений об ошибках, ни воспользовать- ся отладчиком для их исправления.
Async не решает все проблемы

ГЛАВА 2.
Зачем делать
программу асинхронной
Асинхронное программирование – вещь важная и полезная, но поче- му именно важная, зависит от вида приложения. Некоторые преиму- щества проявляются всегда, но особенно значимы в приложениях, которые вы, возможно, не имели в виду. Поэтому рекомендую прочи- тать эту главу, чтобы лучше понимать контекст в целом.
Приложения с графическим
интерфейсом пользователя
для настольных компьютеров
К приложениям для настольных компьютеров предъявляется одно важное требование – они должны реагировать на действия пользо- вателя. Исследования в области человеко-машинного интерфейса показывают, что пользователь не обращает внимания на медленную работу программы, если она откликается на его действия и – жела- тельно – имеет индикатор хода выполнения.
Но если программа зависает, то пользователь недоволен. Обычно зависания связаны с тем, что программа перестает реагировать на действия пользователя во время выполнения длительной операции, будь то медленное вычисление или операция ввода/вывода, напри- мер обращение к сети.
Все каркасы для организации пользовательского интерфейса в C# работают в одном потоке. Это относится и к WinForms, и к WPF, и к Silverlight. Только этот поток может управлять содержимым окна, распознавать действия пользователя и реагировать на них. Если он занят или блокирован дольше нескольких десятков миллисекунд, то пользователь сочтет, что приложение «тормозит».


19
Аналогия с кафе
Асинхронный код, даже написанный вручную, позволяет потоку пользовательского интерфейса вернуться к своей основной обязан- ности – опросу очереди сообщений и реагированию на появляющиеся в ней события. Он также может анимировать ход выполнения задачи, а в последних версиях Windows еще и наведение мыши на различные объекты. То и другое служит для пользователя наглядным подтверж- дением того, что программа работает.
Наличие только одного потока пользовательского интерфейса позволяет упростить синхронизацию. Если бы таких потоков было много, то один мог бы попытаться получить ширину кнопки в момент, когда другой занят размещением элементов управле- ния. Чтобы избежать конфликтов, пришлось бы повсеместно расставлять блокировки; при этом производительность оказа- лась бы не лучше, чем в случае единственного потока.
Аналогия с кафе
Чтобы помочь вам разобраться, прибегну к аналогии. Если вы по- лагаете, что и так всё понимаете, можете спокойно пропустить этот раздел.
Представьте себе небольшое кафе, которое предлагает тосты на за- втрак. Функции официантки выполняет сам хозяин. Он очень серьез- но относится к качеству обслуживания клиентов, но об асинхронной обработке слыхом не слыхивал.
Поток пользовательского интерфейса как раз и моделирует дейс- твия хозяина кафе. Если в компьютере вся работа выполняется по- токами, то в кафе – обслуживающим персоналом. В данном случае персонал состоит из одного-единственного человека, которого можно уподобить единственному потоку пользовательского интерфейса.
Первый посетитель заказывает тост. Хозяин берет ломтик хлеба, включает тостер и ждет, пока тот поджарит хлеб. Посетитель спра- шивает, где взять масло, но хозяин его игнорирует – он всецело пог- лощен наблюдением за тостером, иначе говоря – блокирован. Через пять минут тост готов, и хозяин подает его посетителю. К этому мо- менту уже скопилась очередь, а посетитель обижен, что на него не об- ращают внимания. Ситуация далека от идеала.
Посмотрим, нельзя ли научить хозяина кафе действовать асинх- ронно.
Во-первых, необходимо, чтобы сам тостер мог работать асинхрон- но. При написании асинхронного кода мы должны позаботиться о

20
Глава 2. Зачем делать программу асинхронной том, чтобы запущенная нами длительная операция могла выполнить обратный вызов по завершении. Точно так же, у тостера должен быть таймер, а поджаренный хлеб должен выскакивать с громким звуком, так чтобы хозяин заметил это.
Теперь хозяин может включить тостер и на время забыть о нем, вер- нувшись к обслуживанию клиентов. Точно так же, наш асинхронный код должен возвращать управление сразу после запуска длительной операции, чтобы поток пользовательского интерфейса мог реагиро- вать на действия пользователя. Тому есть две причины:
• у пользователя остается впечатление, что интерфейс «отзыв- чивый», – клиент может попросить масло, и его не проигнори- руют;
• пользователь может одновременно начать другую операцию – следующий клиент может изложить свой заказ.
Теперь хозяин кафе может одновременно обслуживать нескольких клиентов; единственным ограничением является количество тосте- ров и время, необходимое для подачи готового теста. Однако при этом возникают новые проблемы: необходимо помнить, кому какой тост предназначен. На самом деле, поток пользовательского интерфейса, вернувшись к обслуживанию очереди событий, ничего не помнит о том, завершения каких операций он ждет.
Поэтому мы должны связать с запускаемой задачей обратный вы- зов, который известит нас о том, что задача завершилась. Хозяину кафе достаточно прикрепить к тосту листочек с именем клиента. Нам же может потребоваться нечто более сложное, и в общем случае хоте- лось бы иметь возможность задавать произвольные инструкции, что делать по завершении задачи.
Последовав нашим советам, хозяин кафе стал работать полностью асинхронно, и его дело процветает. Довольны и клиенты. Ждать при- ходится меньше, и их просьбы больше не игнорируются. Надеюсь, эта аналогия помогла вам лучше понять, почему асинхронность так важ- на в приложениях с пользовательским интерфейсом.
Серверный код веб-приложения
У ASP.NET-приложений на веб-сервере нет ограничения на единс- твенный поток, как в случае программ с пользовательским интерфей- сом. И тем не менее асинхронное выполнение может оказаться весь- ма полезным, так как для таких приложений характерны длительные операции, особенно запросы к базе данных.