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

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

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

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

Добавлен: 10.11.2023

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

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

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

21
Еще одна аналогия: кухня в ресторане
В зависимости от версии IIS может быть ограничено либо общее число потоков, обслуживающих веб-запросы, либо общее число од- новременно обрабатываемых запросов. Если большая часть времени обработки запроса уходит на обращение к базе данных, то увеличение числа одновременно обрабатываемых запросов может повысить про- пускную способность сервера.
Когда поток блокирован в ожидании какого-то события, он не пот- ребляет процессорное время. Но не следует думать, что он вообще не потребляет ресурсы сервера. На самом деле, любой поток, даже бло- кированный, потребляет два ценных ресурса.
Память
В Windows для каждого управляемого потока резервируется примерно один мегабайт виртуальной памяти. Если количест- во потоков исчисляется десятками, то это не страшно, но когда их сотни, то может превратиться в проблему. Если операцион- ная система вынуждена выгружать память на диск, то возоб- новление потока резко замедляется.
Ресурсы планировщика
Планировщик операционной системы отвечает за выделение потокам процессоров. Планировщик должен рассматривать даже заблокированные потоки, иначе он не будет знать, когда они разблокируются. Это замедляет контекстное переключе- ние, а, значит, и работу системы в целом.
В совокупности эти накладные расходы означают дополнительную нагрузку на сервер, а, стало быть, увеличивают задержку и снижают пропускную способность.
Помните: основная отличительная особенность асинхронного кода состоит в том, что поток, начавший длительную операцию, освобож- дается для других дел. В случае ASP.NET этот поток берется из пула потоков, поэтому после запуска длительной операции он сразу же воз- вращается в пул и может затем использоваться для обработки других запросов. Таким образом, для обработки одного и того же количества запросов требуется меньше потоков.
Еще одна аналогия: кухня
в ресторане
Веб-сервер можно рассматривать как модель ресторана. Есть много клиентов, заказывающих еду, а кухня пытается обслужить все заказы как можно быстрее.

22
Глава 2. Зачем делать программу асинхронной
На нашей кухне много поваров, каждый из которых можно уподо- бить потоку. Повар готовит заказанные блюда, но в течение какого- то времени любое блюдо должно постоять на плите, а повару в этот момент делать нечего. Точно так же обстоит дело с веб-запросом, для обработки которого нужно обратиться к базе данных, – веб-сервер к этому отношения не имеет.
При блокирующей организации работ на кухне повар будет стоять у плиты, дожидаясь готовности блюда. Чтобы добиться точной анало- гии с заблокированным потоком, которому не выделяется процессор- ное время, предположим, что повару ничего не платят за время, когда он ждет готовности. Быть может, в это время он читает газету.
Но даже если повару не нужно платить и для приготовления каж- дого блюда можно нанять нового повара, простаивающие в ожидании повара занимают место на кухне. В одну кухню не удастся впихнуть больше нескольких десятков поваров, иначе в ней будет трудно пере- мещаться, и вся работа станет.
Разумеется, асинхронная система работает куда лучше. Ставя блю- до на плиту или в духовку, повар помечает, что это за блюдо и на какой стадии приготовления оно находится, а затем начинает заниматься другим делом. Когда подойдет время снимать блюдо с плиты, это смо- жет сделать любой повар, – он и продолжит его готовить.
Этот подход может быть эффективно применен в веб-серверах. Не- сколько потоков могут справиться с обработкой того же количества одновременных запросов, для которого раньше требовались сотни потоков (а то и вообще не удавалось обработать). На самом деле, в некоторых каркасах для создания веб-серверов, например в node.js, отвергается сама идея о наличии нескольких потоков, и все запросы асинхронно обрабатываются единственным потоком. Зачастую при этом удается обработать больше запросов, чем может многопоточная, но блокирующая система. Точно так же, единственный повар в пус- той кухне, хорошо организовавший свою работу, сможет приготовить больше блюд, чем сотня поваров, которые только и делают, что меша- ют друг другу или почитывают газетку.
Silverlight, Windows Phone
и Windows 8
Проектировщики Silverlight, безусловно, знали о преимуществах асинхронного кода в приложениях с пользовательским интерфей- сом, поэтому решили поощрить такой подход. Для этого они просто


23
Параллельный код изъяли из каркаса большую часть синхронных API. Так, например, веб-запросы можно отправлять только асинхронно.
Асинхронный код «заразен». Если где-то вызвать какой-нибудь асинхронный API, то и вся программа естественно становится асинх- ронной. Поэтому в Silverlight вы обязаны писать асинхронный код – альтернативы просто не существует. Возможно, и существует метод
Wait или иной способ работать с асинхронным API синхронно, при- остановив выполнение в ожидании обратного вызова. Но, поступая так, вы теряете все преимущества, о которых я говорил выше.
Silverlight для Windows Phone, как следует из названия, является разновидностью Silverlight. Правда, включены дополнительные API, небезопасные в среде браузера, например TCP-сокеты. Но и в этом случае предоставляются только асинхронные версии API, что застав- ляет вас писать асинхронный код. И уж для мобильного устройства, располагающего крайне ограниченными ресурсами, это вдвойне оп- равдано. Запуск дополнительных потоков весьма негативно отража- ется на времени работы аккумулятора.
И в Windows 8, технически не связанной с Silverlight, подход такой же. Количество различных API в этом случае гораздо больше, но для методов, которые могут выполняться дольше 50 мс, предоставляются только асинхронные версии.
Параллельный код
Современные компьютеры оснащаются несколькими процессорны- ми ядрами, работающими независимо друг от друга. Желательно, чтобы программа могла задействовать имеющиеся ядра, но два ядра не могут писать в одну и ту же ячейку памяти, так как это чревато повреждением ее содержимого.
Быть может, было бы лучше применять чистое (иначе говоря, функциональное) программирование, при котором не существу- ет побочных эффектов, то есть состояние памяти не изменяется.
Это помогло бы полнее воспользоваться преимуществами па- раллелизма, но для некоторых программ неприемлемо. Пользо- вательским интерфейсам состояние необходимо. Базы данных сами по себе являются состоянием.
Стандартное решение предполагает использование взаимно ис- ключающих блокировок (мьютексов) в случаях, когда несколько ядер потенциально могут обращаться к одной и той же ячейке памяти. Но тут есть свои проблемы. Часто бывает так, что программа захватывает

24
Глава 2. Зачем делать программу асинхронной блокировку, а затем вызывает метод или генерирует событие, в кото- ром захватывается другая блокировка. Иногда удерживать сразу две блокировки необязательно, но так код оказывается проще. В резуль- тате другим потокам приходится ждать освобождения блокировки, хотя они могли бы в это время заниматься полезной работой. Хуже того, иногда возникает ситуация, когда каждый поток ждет освобож- дения блокировки, занятой другим, а это приводит к взаимоблоки- ровке (deadlock). Такие ошибки трудно предвидеть, воспроизводить и исправлять.
Одно из самых многообещающих решений – модель вычислений, основанная на акторах. При таком подходе каждый участок записы- ваемой памяти принадлежит ровно одному актору. Единственный способ записать в эту память – отправлять актору-владельцу сооб- щения, которые он обрабатывает поочередно и, возможно, посылает сообщения в ответ. Но это как раз и есть асинхронное программиро- вание. Запрос действия у актора – типичная асинхронная операция, поскольку мы можем заниматься другими вещами, пока не придет ответное сообщение. А значит, для программирования такой модели можно использовать механизм async, как мы и увидим в главе 10.
Пример
Рассмотрим пример приложения для ПК, которое откровенно нуж- дается в переходе на асинхронный стиль. Его исходный код можно скачать по адресу https://bitbucket.org/alexdavies74/faviconbrowser.
Рекомендую сделать это (если вы не пользуетесь системой управле- ния версиями Mercurial, то код можно скачать в виде zip-файла) и открыть решение в Visual Studio. Необходимо скачать ветвь default
, содержащую синхронную версию.
Запустив программу, вы увидите окно с кнопкой. Нажатие этой кнопки приводит к отображению значков нескольких популярных сайтов. Для этого программа скачивает файл favicon.ico
, присутст- вующий на большинстве сайтов (рис. 2.1).
Приглядимся к коду. Наиболее важная его часть – метод, который загружает значок и добавляет его на панель WrapPanel (отметим, что это приложение WPF).
private void AddAFavicon(string domain)
{
WebClient webClient = new WebClient();
byte[] bytes = webClient.DownloadData(“http://”+domain+”/favicon.ico”);


25
Image imageControl = MakeImageControl(bytes);
m_WrapPanel.Children.Add(imageControl);
}
Рис. 2.1. Обозреватель значков сайтов
Обратите внимание, что эта реализация полностью синхронна.
Поток блокируется на время скачивания значка. Вероятно, вы заме- тили, что в течение нескольких секунд после нажатия кнопки окно ни на что не реагирует. Как вы теперь понимаете, объясняется это тем, что поток пользовательского интерфейса блокирован и не обрабаты- вает события, генерируемые действиями пользователя.
В следующих главах мы преобразуем эту синхронную программу в асинхронную.
Пример

1   2   3   4   5   6   7   8   9

ГЛАВА 3.
Написание асинхронного
кода вручную
В этой главе мы поговорим о написании асинхронного кода без помо- щи со стороны C# 5.0 и async. В некотором смысле это обзор техники, которую вы вряд ли будете когда-либо использовать, но знать о ней надо, чтобы понимать, что происходит за кулисами. Поэтому я не ста- ну задерживаться надолго, а только отмечу моменты, необходимые для понимания.
О некоторых асинхронных
паттернах в .NET
Как я уже отмечал, Silverlight предоставляет только асинхронные версии таких API, как доступ к сети. Вот, например, как можно было бы скачать и отобразить веб-страницу:
private void DumpWebPage(Uri uri)
{
WebClient webClient = new WebClient();
webClient.DownloadStringCompleted += OnDownloadStringCompleted;
webClient.DownloadStringAsync(uri);
}
private void OnDownloadStringCompleted(object sender,
DownloadStringCompletedEventArgs eventArgs)
{
m_TextBlock.Text = eventArgs.Result;
}
API такого вида называется асинхронным событийно-управляе-
мым паттерном (Event-based Asynchronous Pattern – EAP). Идея в том, чтобы вместо одного синхронного метода, который блокирует исполнение программы до тех пор, пока не будет скачана вся стра-

27
О некоторых асинхронных паттернах в .NET
ница, используется один метод и одно событие. Сам метод выглядит, как синхронный аналог, только не возвращает никакого значения.
Аргументом же события является специальный подкласс
EventArgs
, который содержит скачанную строку.
Перед вызовом метода мы подписываемся на событие. Метод, будучи асинхронным, возвращает управление немедленно. Затем в какой-то момент генерируется событие, которое мы можем обрабо- тать.
Очевидно, что этот паттерн не слишком удобен и не в последнюю очередь из-за того, что приходится разбивать простую последователь- ность команд на два метода. Не упрощает дела и тот факт, что нужно подписаться на событие. Если вы захотите использовать тот же са- мый экземпляр
WebClient для отправки другого запроса, то будете неприятно удивлены тем, что старый обработчик события все еще присоединен и будет вызван снова.
Еще один асинхронный паттерн, встречающийся в .NET, подразу- мевает использование интерфейса
IAsyncResult
. В качестве примера упомянем метод класса
Dns
, который ищет IP-адрес по имени хоста, –
BeginGetHostAddresses
. В этом случае предоставляются два метода:
BeginMethodName
начинает операцию, а
EndMethodName
– обратный вызов, которому передается результат.
private void LookupHostName()
{
object unrelatedObject = “hello”;
Dns.BeginGetHostAddresses(“oreilly.com”, OnHostNameResolved,
unrelatedObject);
}
private void OnHostNameResolved(IAsyncResult ar)
{
object unrelatedObject = ar.AsyncState;
IPAddress[] addresses = Dns.EndGetHostAddresses(ar);
// Обработать адрес
}
Этот вариант, по крайней мере, не страдает от проблем, связанных с присоединенными ранее обработчиками событий. Но неоправдан- ная сложность API – два метода вместо одного – все равно осталась, и мне это кажется неестественным.
В обоих случаях приходится разбивать логически единую проце- дуру на два метода. Интерфейс
IAsyncResult позволяет передавать данные из первого метода во второй, что я продемонстрировал на при-


28
Глава 3. Написание асинхронного кода вручную мере строки
“hello”
. Но сделано это неудобно, поскольку вы должны передать что-то, даже если это не нужно, и выполнить приведение к нужному типу из object
. Паттерн EAP также поддерживает передачу объекта – и столь же неэлегантно.
Передача контекста из одного метода в другой – общая проблема, свойственная всем асинхронным паттернам. В следующем разделе мы увидим, что решение дают лямбда-функции, которые можно ис- пользовать в обоих случаях.
Простейший асинхронный
паттерн
Пожалуй, проще всего реализовать асинхронное поведение без ис- пользования async, передав обратный вызов в качестве параметра метода:
void GetHostAddress(string hostName, Action callback)
Мне кажется, что с точки зрения простоты он превосходит все ос- тальные варианты.
private void LookupHostName()
{
GetHostAddress(“oreilly.com”, OnHostNameResolved);
}
private void OnHostNameResolved(IPAddress address)
{
// Обработать адрес
}
Как я уже отмечал, вместо двух методов можно использовать в ка- честве обратного вызова анонимный метод или лямбда-выражение.
У такого подхода есть важное достоинство – возможность обращать- ся к переменным, объявленным в первой части метода.
private void LookupHostName()
{
int aUsefulVariable = 3;
GetHostAddress(“oreilly.com”, address =>
{
// Сделать что-то с адресом и переменной aUsefulVariable
});
}

29
Введение в класс Task
Правда, лямбда-выражения несколько сложновато читать, а если используется несколько асинхронных API, то потребуется много вложенных лямбда-выражений. Поэтому количество уровней отступа быстро возрастает, и с кодом становится трудно работать.
Недостаток этого простого подхода заключается в том, что вызы- вающая программа больше не получает исключений. В паттернах, ис- пользуемых в .NET, вызов метода
EndMethodName
или чтение свойст- ва
Result приводит к повторному возбуждению исключения, которое ваш код может обработать. В противном случае исключение возник- нет в неожиданном месте или останется необработанным вовсе.
Введение в класс Task
Библиотека Task Parallel Library была включена в версию .NET Frame- work 4.0. Важнейшим в ней является класс
Task
, представляющий выполняемую операцию. Его универсальный вариант,
Task
, играет роль обещания вернуть значение (типа
T
), когда в будущем, по завершении операции, оно станет доступно.
Как мы увидим ниже, механизм async в C# 5.0 активно пользуется классом
Task
. Но и без async классом
Task
, а особенно его вариан- том
Task
, можно воспользоваться при написании асинхронных программ. Для этого нужно запустить операцию, которая возвращает
Task
, а затем вызвать метод
ContinueWith для регистрации об- ратного вызова.
private void LookupHostName()
{
Task ipAddressesPromise =
Dns.GetHostAddressesAsync(“oreilly.com”);
ipAddressesPromise.ContinueWith(_ =>
{
IPAddress[] ipAddresses = ipAddressesPromise.Result;
// Обработать адрес
});
}
Достоинство
Task в том, что теперь требуется вызвать только один метод класса
Dns
, в результате чего API становится чище. Вся логика, относящаяся к обеспечению асинхронного поведения, инкапсулиро- вана в классе
Task и дублировать ее в каждом асинхронном методе нет необходимости. Этот класс умеет в частности обрабатывать ис- ключения и работать с контекстами синхронизации
Synchroniza-