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

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

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

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

Добавлен: 10.11.2023

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

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

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
ГЛАВА 11.
Автономное тестирование
асинхронного кода
Я хочу коротко остановиться на вопросе об автономном тестирова- нии асинхронного кода. Простейший подход работает не очень хоро- шо, но при поддержке со стороны каркаса автономного тестирования нетрудно написать тесты, вызывающие async-методы.
Проблема автономного
тестирования в асинхронном
окружении
Async-методы быстро возвращают управление, обычно вместе с объ- ектом
Task
, который станет завершенным в будущем. К такому объ- екту мы, как правило, применяем оператор await
; посмотрим, как это выглядит в автономном тесте.
[TestMethod]
public async void AlexsTest()
{
int x = await AlexsMethod();
Assert.AreEqual(3, x);
}
Чтобы можно было использовать оператор await
, я пометил тес- товый метод ключевым словом async
. Но у этого действия есть очень важный побочный эффект. Теперь тестовый метод также быстро воз- вращает управление, а завершается в будущем. Фактически оказы- вается, что тестовый метод возвращается, не возбуждая исключений, сразу по достижении await
, поэтому каркас тестирования – в данном случае MSTest – помечает его как успешный.

91
Поскольку тестовый метод имеет тип async void
, то все возбуж- денные в нем исключения повторно возбуждаются в вызывающем контексте синхронизации, где они либо игнорируются, либо приво- дят к отказу совершенно другого теста, запущенного позднее.
Но настоящая опасность состоит в том, что все тесты считаются успешными вне зависимости от фактического результата.
Написание работающих
асинхронных тестов вручную
Один из способов решить эту проблему – не помечать тестовые ме- тоды ключевым словом async
. Тогда мы должны синхронно ждать результата асинхронного вызова.
[TestMethod]
public void AlexsTest()
{
int x = AlexsMethod().Result;
Assert.AreEqual(3, x);
}
Чтение свойства
Result приводит к блокирующему ожиданию за- вершения задачи
Task
. Это работает и тест благополучно не проходит, если не должен проходить. Если метод
AlexsMethod возбудит исклю- чение, то оно будет повторно возбуждено при обращении к
Result
, правда, в отличие от исключения, повторно возбуждаемого операто- ром await
, оно будет обернуто объектом
AggregateException
Но теперь вы уже понимаете, насколько некрасиво выглядит ис- пользование блокирующего свойства
Result объекта
Task
. К тому же, как мы видели в разделе «Взаимодействие с синхронным кодом», это еще и опасно, если программа работает в однопоточном контексте синхронизации. По счастью, ни один из популярных каркасов авто- номного тестирования по умолчанию не пользуется однопоточным
SynchronizationContext
. И всё равно при таком решении впустую расходуется один поток, так что оно не оптимально.
Поддержка со стороны каркаса
автономного тестирования
Некоторые каркасы автономного тестирования явно поддерживают механизм async. Они позволяют создавать тестовые методы, возвра-
Поддержка со стороны каркаса автономного...


92
Глава 11. Автономное тестирование...
щающие
Task
, и, следовательно, помечать их ключевым словом async.
Каркас будет ждать завершения задачи
Task и только после этого по- метит тест как прошедший и перейдет к следующему тесту.
На момент написания этой книги такой стиль поддерживали кар- касы xUnit.net и MSTest. Я полагаю, что в ближайшее время под- держка будет добавлена и в другие популярные каркасы, пусть даже в виде небольшого дополнительного модуля.
[TestMethod]
public async Task AlexsTest()
{
int x = await AlexsMethod();
Assert.AreEqual(3, x);
}
Пожалуй, это самый чистый способ написания автономных тестов для асинхронного кода, поскольку ответственность за управление по- токам перекладывается на каркас тестирования.

ГЛАВА 12.
Механизм async
в приложениях ASP.NET
Большинство разработчиков на платформе .NET пишут веб-прило- жения. Механизм async открывает новые возможности для повыше- ния производительности серверного кода, поэтому рассмотрим эту тему подробнее.
Преимущества асинхронного
веб-серверного кода
При обработке запроса веб-сервером отзывчивость не играет такой роли, как в программах с пользовательским интерфейсом. Произво- дительность веб-сервера измеряется в терминах пропускной способ- ности, задержки и постоянства этих характеристик.
Асинхронный код на нагруженном веб-сервере требует меньше по- токов, чем синхронный, обслуживающий ту же нагрузку. Каждый по- ток потребляет память, а объем памяти часто является узким местом веб-сервера. Если памяти на хватает, то сборщик мусора запускается чаще и вынужден выполнять больше работы. Если недостаточно фи- зической памяти, то начинается выгрузка страниц на диск, а в случае, когда выгруженные страницы скоро снова оказываются необходимы, работа системы существенно замедляется.
Возможность писать асинхронный веб-серверный код появилась в
ASP.NET начиная с версии 2.0, но без поддержки со стороны языка это было нелегко. Большинство разработчиков считало, что проще и рентабельнее установить дополнительные серверы и балансировать нагрузку между ними. Но с выходом C# 5.0 и .NET 4.5 писать асин- хронный код стало настолько просто, что каждому имеет смысл вос- пользоваться присущей ему эффективностью.


94
Глава 12. Механизм async в приложениях ASP.NET
Использование async
в ASP.NET MVC 4
Версия ASP.NET MVC 4 и более поздние при запуске на платформе
.NET 4.5 и выше в полной мере поддерживают паттерн TAP, поэтому можно использовать async-методы. В приложении MVC важнейшим местом для задействования асинхронности является контроллер.
Методы контроллера можно пометить ключевым словом async и воз- вращать из них значение типа
Task
:
public class HomeController : Controller
{
public async Task Index()
{
ViewBag.Message = await GetMessageAsync();
return View();
}
Это решение опирается на тот факт, что для запросов, занимаю- щих длительное время, желательно иметь асинхронный API. Многие системы объектно-реляционного отображения (ORM) пока не под- держивают асинхронных вызовов, но в API, основанный на классе
.NET
SqlConnection
,
такая поддержка встроена.
Использование async
в предыдущих версиях
ASP.NET MVC
В версиях младше MVC 4 поддержка асинхронных контроллеров не основана на паттерне TAP и устроена более сложно. Вот один из спо- собов адаптировать TAP-метод контроллера в духе MVC 4 к паттерну, применявшемуся в предыдущих версиях MVC.
public class HomeController : AsyncController
{
public void IndexAsync()
{
AsyncManager.OutstandingOperations.Increment();
Task task = IndexTaskAsync();
task.ContinueWith(_ =>
{
AsyncManager.Parameters[“result”] = task.Result;

95
Использование async в ASP.NET Web Forms
AsyncManager.OutstandingOperations.Decrement();
});
}
public ActionResult IndexCompleted(ActionResult result)
{
return result;
}
private async Task IndexTaskAsync()
{
Во-первых, класс контроллера должен наследовать классу
Async-
Controller
, а не
Controller
, так как именно это позволяет применить асинхронный паттерн. А означает этот паттерн, что для каждого действия существуют два метода:
ActionAsync и
ActionComplet- ed
. Объект
AsyncManager управляет временем жизни асинхронного запроса. Когда свойство
OutstandingOperations обращается в нуль, вызывается метод
ActionCompleted
. Работа с объектом
Task организуется вручную с помощью метода
ContinueWith
, а результат передается методу
ActionCompleted в словаре
Parameters
Для простоты я опустил в этом примере обработку исключений.
В общем и целом, решение не блещет красотой, но, подготовив такую инфраструктуру, все же можно использовать async-методы.
Использование async
в ASP.NET Web Forms
Для стандартных вариантов ASP.NET и Web Forms не существует версий, отдельных от версии каркаса .NET, с которой они поставля- ются. В .NET 4.5 ASP.NET в классе
Page поддерживаются методы типа async void
, например
Page_Load protected async void Page_Load(object sender, EventArgs e)
{
Title = await GetTitleAsync();
}
Такая реализация может показаться странной. Как ASP.NET узна- ет, что метод типа async void завершился? Было бы более естествен- но возвращать задачу
Task
, завершения которой ASP.NET могла бы дождаться, перед тем как приступать к отрисовке страницы, – так же, как это делается в MVC 4. Тем не менее, возможно, из соображений


96
Глава 12. Механизм async в приложениях ASP.NET
обратной совместимости, требуется, чтобы методы возвращали void
А для решения указанной проблемы в ASP.NET используется специ- альный подкласс
SynchronizationContext
, который ведет учет всем асинхронным операциям и позволяет перейти к следующему этапу обработки только после того, как все они завершены.
При исполнении асинхронного кода в контексте синхронизации
ASP.NET имейте в виду, что этот контекст однопоточный. Попытка организовать блокирующее ожидание
Task
, например путем чтения свойства
Result
, скорее всего, приведет к взаимоблокировке, так как операторы await с более глубоким уровнем вложенности не смогут воспользоваться контекстом
SynchronizationContext для возоб- новления.

ГЛАВА 13.
Механизм async
в приложениях WinRT
Для читателей, не знакомых с WinRT, приведу краткий обзор этой технологии.
Что такое WinRT?
WinRT (или Windows Runtime) – это группа API, используемых в приложениях, работающих на платформах Windows 8 и Windows RT для процессоров ARM. Одна из проектных целей WinRT API – обес- печить отзывчивость за счет асинхронного программирования. Все методы, для выполнения которых может потребоваться более 50 мс, асинхронны.
При проектировании WinRT ставилась задача обеспечить едино- образный доступ из трех разных технологий: .NET, JavaScript и ма- шинный код (обычно на C++). Для этого все API определены в еди- ном формате метаданных, который называется WinMD. Программу на любом из перечисленных языков можно откомпилировать вместе с
WinMD-определением API, не прибегая к зависящим от языка оберт- кам. Этот подход называется проецированием – каждый компилятор или интерпретатор проецирует тип WinRT на тип своего языка.
Формат WinMD основан на формате метаданных в сборках .NET, поэтому имеющиеся конструкции очень похожи на применяемые в .NET: классы, интерфейсы, методы, свойства, атрибуты и т. д.
Но есть и различия; например, универсальные типы допустимы, а универсальные методы – нет.
Большая часть WinRT реализована на машинном языке, но можно писать WinRT-компоненты также на C#, и эти компоненты будут до- ступны из любого поддерживаемого языка.

98
Глава 13. Механизм async в приложениях WinRT
Поскольку интерфейсы WinRT не ориентированы специально на .NET, в API написанного вами WinRT-компонента не могут использоваться многие типы .NET. Многие интерфейсы коллекций, например
IList
, проецируются автоматически. Но не класс
Task
, в котором слишком много специфичного для .NET поведения.
Интерфейсы IAsyncAction
и IAsyncOperation
Эти два интерфейса WinRT эквивалентны соответственно
Task и
Task
. В асинхронных методах WinRT используется паттерн, аналогичный TAP, только методы должны возвращать значения типа
IAsyncAction или
IAsyncOperation
. Оба интерфейса очень похожи, поэтому далее в этой главе, упоминая любой из них, я буду иметь в виду оба.
Ниже приведен пример метода из класса WinRT
Syndication-
Client
, который читает RSS-ленту.
IAsyncOperation RetrieveFeedAsync(Uri uri)
Напомню, что
IAsyncAction и IAsyncOperation – интер- фейсы WinMD, а не .NET. Это различие несколько сбивает с тол- ку, потому что те и другие можно использовать в программах на
C#, как обычные интерфейсы .NET.
Как и TAP-методы, методы WinRT такого вида сразу же возвращают значение типа
IAsyncOperation
, которое трактуется как обещание вернуть объект
SyndicationFeed в будущем. Ключевое слово await можно использовать для объектов типа
IAsyncOperation точно так же, как для объектов
Task
, то есть метод
RetrieveFeedAsync можно вызвать следующим образом:
SyndicationFeed feed = await rssClient.RetrieveFeedAsync(url);
Оператор await применим к любому типу, обладающему ме- тодами специального вида, которые обеспечивают требуемое по- ведение. В классе
Task такие методы есть, а в интерфейсе
IAsync-
Operation
– нет. Однако удовлетворить требованиям паттерна можно и за счет методов расширения
IAsyncOperation
, которые
.NET предоставляет для согласования с await
Иногда необходим доступ к объекту
Task
, представляющему асинхронный вызов WinRT; например, для передачи комбинато- ру
Task.WhenAll или для использования метода
Confi gureAwait


99
Информирование о ходе выполнения
Для создания такого объекта служит еще один метод расширения
IAsyncOperation

AsTask
:
Task task = rssClient.RetrieveFeedAsync(url).AsTask();
Метод
AsTask возвращает обычный объект
Task
, который можно использовать как угодно.
Отмена
В версии TAP для WinRT выбран другой подход к отмене операции.
Если в .NET TAP в качестве дополнительного параметра методу пе- редается объект
CancellationToken
, то в WinRT механизм отмены встроен в возвращаемый объект типа
IAsyncOperation
IAsyncOperation op = rssClient.RetrieveFeedAsync(url);
op.Cancel();
Благодаря этому все асинхронные методы в WinRT допускают возможность отмены. Действительно ли они прекращают работу при вызове
Cancel
– это другой вопрос; я полагаю, что не все.
У такого решения по сравнению с
CancellationToken есть плюсы и минусы, поэтому не удивительно, что для TAP было вы- брано одно, а для WinRT – другое. Использование
Cancella- tionToken упрощает применение одного маркера к нескольким методам, тогда как встраивание отмены в тип возвращенного обещания делает API чище.
Впрочем, использовать метод
Cancel напрямую не следует, пото- му что у метода расширения
AsTask имеется перегруженный вариант, которые принимает стандартный тип .NET
CancellationToken и ор- ганизует все необходимые действия автоматически:
... = await rssClient.RetrieveFeedAsync(url).AsTask(cancellationToken);
Теперь можно использовать класс
CancellationTokenSource
, как обычно.
Информирование о ходе
выполнения
И в этом случае в асинхронных методах WinRT применен иной под- ход, нежели в TAP. В WinRT механизм информирования о ходе вы-

100
Глава 13. Механизм async в приложениях WinRT
полнения встроен в тип возвращаемого обещания. Но поскольку эта возможность факультативна, методы, информирующие о ходе вы- полнения, возвращают специализированные интерфейсы:

IAsyncActionWithProgress

IAsyncOperationWithProgress
Они очевидным образом соответствуют интерфейсам
IAsyncAc- tion и
IAsyncOperation
и добавляют к ним лишь событие, ко- торое генерируется, когда информация о ходе выполнения изменя- ется.
Подписаться на это событие проще всего, воспользовавшись еще одним перегруженным вариантом
AsTask
, который принимает значе- ние стандартного типа .NET
IProgress
и организует всю необхо- димую обвязку:
... = await rssClient.RetrieveFeedAsync(url).AsTask(progress);
Разумеется, существует также перегруженный вариант, который принимает одновременно
CancellationToken и
IProgress
Реализация асинхронных
методов в компоненте WinRT
Своей эффективностью WinRT обязана тому, что библиотеки оди- наково просто использовать из любого поддерживаемого языка. При создании собственных библиотек для работы на платформе WinRT вы можете обеспечить такую же гибкость, откомпилировав библио- теку как компонент WinRT, а не как сборку .NET.
В C# сделать это очень просто с одним ограничением: в откры- том интерфейсе компонента должны использоваться только типы
WinMD или автоматически проецируемые компилятором на типы
WinMD. Повторю, что тип
Task не является ни тем, ни другим, поэ- тому необходимо вместо него возвращать
IAsyncOperation
public IAsyncOperation GetTheIntAsync()
{
return GetTheIntTaskAsync().AsAsyncOperation();
}
private async Task GetTheIntTaskAsync()
{