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

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

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

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

Добавлен: 10.11.2023

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

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

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

51
Что специфицировано в TAP?
Вот пример хорошо спроектированного синхронного метода из класса
Dns
:
public static IPHostEntry GetHostEntry(string hostNameOrAddress)
TAP содержит аналогичные рекомендации по проектирования асинхронных методов, основанные на уже имеющихся у вас знаниях о синхронных методах. Перечислим их.
• Параметры должны быть такими же, как у эквивалентного синхронного метода. Параметры со спецификаторами ref и out не допускаются.
• Метод должен возвращать значение типа
Task или
Task
в зависимости от того, возвращает что-то синхронный метод или нет. Задача должна завершаться в какой-то момент в бу- дущем и предоставлять методу значение результата.
• Метод следует называть по образцу
NameAsync
, где
Name
– имя эквивалентного синхронного метода.
• Исключение из-за ошибки в параметрах вызова метода можно возбуждать непосредственно. Все остальные исключения сле- дует помещать в объект
Task
Ниже приведена сигнатура метода, следующая рекомендациям
TAP:
public static Task GetHostEntryAsync(string hostNameOrAddress)
Все это может показаться очевидным, но, как мы видели в разде- ле «О некоторых асинхронных паттернах в .NET», TAP – уже третий формальный асинхронный паттерн из имеющихся в каркасе .NET, и я уверен, что есть еще бесчисленное множество неформализованных способов написания асинхронного кода.
Основная идея TAP заключается в том, что асинхронный метод должен возвращать объект типа
Task
, инкапсулирующий обещание длительной операции завершиться в будущем. Без этого более ран- ним асинхронным паттернам приходилось вводить дополнительные параметры метода либо включать в интерфейс дополнительные ме- тоды или события, чтобы поддержать механизм обратных вызовов.
Объект
Task может содержать всю инфраструктуру, необходимую для обратных вызовов, не засоряя код техническими деталями.
У такого подхода есть и еще одно достоинство: поскольку меха- низм асинхронных вызовов теперь находится в
Task
, нет нужды дублировать его при каждом асинхронном вызове. Это в свою очередь

52
Глава 6. Паттерн TAP
означает, что механизм можно сделать более сложным и мощным, в частности восстанавливать различные контексты, например контекст синхронизации, перед обратным вызовом. Наконец, TAP предлагает единый API для работы с асинхронными операциями, что позволяет реализовать такие средства, как async, на уровне компилятора; при использовании прежних паттернов это было невозможно.
Использование Task
для операций, требующих
большого объема вычислений
Иногда длительная операция не отправляет запросов в сеть и не об- ращается к диску, а просто выполняет продолжительное вычисление.
Разумеется, нельзя рассчитывать на то, что при этом удастся обойтись без занятия потока, как в случае сетевого доступа, но желательно все же избежать зависания пользовательского интерфейса. Для этого мы должны вернуть управление потоку пользовательского интерфейса, чтобы он мог обрабатывать другие события, а длительное вычисле- ние производить в отдельном потоке.
Класс
Task позволяет это сделать, а для обновления пользователь- ского интерфейса по завершении вычисления мы можем, как обычно, использовать await
:
Task t = Task.Run(() => MyLongComputation(a, b));
Метод
Task.Run исполняет переданный ему делегат в потоке, взя- том из пула
ThreadPool
. В данном случае я воспользовался лямбда- выражением, чтобы упростить передачу счетной задаче локальных переменных. Возвращенная задача
Task запускается немедленно, и мы можем дождаться ее завершения, как любой другой задачи:
await Task.Run(() => MyLongComputation(a, b));
Это очень простой способ выполнить некоторую работу в фоно- вом потоке.
Если необходим более точный контроль над тем, какой поток про- изводит вычисления или как он планируется, в классе
Task имеется статическое свойство
Factory типа
TaskFactory
. У него есть метод
StartNew с различными перегруженными вариантами для управле- ния вычислением:


53
Создание задачи-марионетки
Task t = Task.Factory.StartNew(() => MyLongComputation(a, b),
cancellationToken,
TaskCreationOptions.LongRunning,
taskScheduler);
Если вы занимаетесь разработкой библиотеки, в которой мно- го счетных методов, то может возникнуть соблазн предоставить их асинхронные версии, которые вызывают
Task.Run для выполнения работы в фоновом потоке. Это неудачная идея, потому что пользова- тель вашего API лучше вас знает о требованиях приложения к орга- низации потоков. Например, в веб-приложениях использование пула потоков не дает никакого выигрыша; единственное, что следует оп- тимизировать, – это общее число потоков. Вызвать метод
Task.Run очень просто, поэтому оставьте это решение на усмотрение пользо- вателя.
Создание задачи-марионетки
Методы, написанные в соответствии с TAP, настолько просто исполь- зовать, что вы естественно захотите оформлять свои API именно так.
Мы уже знаем, как потреблять другие TAP API с помощью async-ме- тодов. Но что, если для какой-то длительной операции еще не напи- сан TAP API? Быть может, в реализации ее API использован какой- то другой асинхронный паттерн. А быть может, нет вообще никакого
API, и вы делаете нечто асинхронное вручную.
На этот случай предусмотрен класс
TaskCompletionSource
, позволяющий создать задачу
Task
, которой вы управляете как ма- рионеткой. Вы можете в любой момент сделать эту задачу успешно завершившейся. Или записать в нее исключение и тем самым сказать, что она завершилась с ошибкой.
Рассмотрим пример. Предположим, требуется инкапсулировать показываемый пользователю вопрос в следующем методе:
Task GetUserPermission()
Вопрос представляет собой написанный вами диалог, в котором у пользователя запрашивается какое-то разрешение. Поскольку запра- шивать разрешение нужно в разных местах приложения, важно, что- бы метод было просто вызывать. Идеальная ситуация для использо- вания асинхронного метода, так как мы не хотим, чтобы этот диалог отображался в потоке пользовательского интерфейса. Однако этот метод очень далек от традиционных асинхронных методов, в которых

54
Глава 6. Паттерн TAP
производится обращение к сети или еще какая-то длительная опера- ция. В данном случае мы ждем ответа от пользователя. Рассмотрим тело метода.
private Task GetUserPermission()
{
// Создать объект TaskCompletionSource, чтобы можно было вернуть
// задачу-марионетку
TaskCompletionSource tcs = new TaskCompletionSource();
// Создать диалог
PermissionDialog dialog = new PermissionDialog();
// Когда пользователь закроет диалог, сделать задачу завершившейся
// с помощью метода SetResult dialog.Closed += delegate { tcs.SetResult(dialog.PermissionGranted); };
// Показать диалог на экране dialog.Show();
// Вернуть еще не завершившуюся задачу-марионетку return tcs.Task;
}
Обратите внимание, что метод не помечен ключевым словом async
; мы создаем объект
Task вручную и не нуждаемся в помощи компилятора.
TaskCompletionSource создает объект
Task и предоставляет к нему доступ через свойство
Task
. Мы возвраща- ем этот объект, а позже делаем его завершившимся, вызывая метод
SetResult объекта
TaskCompletionSource
Поскольку мы следовали паттерну TAP, вызывающая программа может просто ждать разрешения пользователя с помощью await
. Код получается весьма элегантным:
if (await GetUserPermission())
{ ....
Вызывает раздражение отсутствие неуниверсальной версии клас- са
TaskCompletionSource
. Однако
Task
– подкласс
Task
, поэтому его можно использовать всюду, где требуется объект
Task
Это в свою очередь означает, что можно воспользоваться классом
TaskCompletionSource
, и рассматривать объект типа
Task
, являющийся значением свойства
Task
, как объект типа
Task
. Я обыч- но работаю с конкретизацией
TaskCompletionSource
и для ее заполнения вызываю
SetResult(null)
. При желании нетрудно создать неуниверсальную версию
TaskCompletionSource
, основыва- ясь на универсальной.


55
Взаимодействие с прежними асинхронными...
Взаимодействие с прежними
асинхронными паттернами
Разработчики .NET создали TAP-версии для всех важных асинхрон- ных API, встречающихся в каркасе. Но на случай, если вам придется взаимодействовать с каким-нибудь написанным ранее асинхронным кодом, полезно знать, как реализовать согласованный с TAP метод на основе варианта, не согласованного с TAP. Кстати, это еще и интерес- ный пример использования класса
TaskCompletionSource
Обратимся к примеру поиска в DNS, который я приводил выше.
В .NET 4.0 API асинхронного поиска в DNS был основан на приме- нении паттерна
IAsyncResult
, то есть фактически состоит из пары методов –
Begin и
End
:
IAsyncResult BeginGetHostEntry(string hostNameOrAddress,
AsyncCallback requestCallback,
object stateObject)
IPHostEntry EndGetHostEntry(IAsyncResult asyncResult)
Обычно для использования такого API применяется лямбда- выражение, внутри которого вызывается метод
End
. Так мы и пос- тупим, только в обратном вызове не будем делать ничего содер- жательного, а просто завершим задачу
Task с помощью
TaskComp- letionSource
public static Task GetHostEntryAsync(string hostNameOrAddress)
{
TaskCompletionSource tcs =
new TaskCompletionSource();
Dns.BeginGetHostEntry(hostNameOrAddress, asyncResult =>
{
try
{
IPHostEntry result = Dns.EndGetHostEntry(asyncResult);
tcs.SetResult(result);
}
catch (Exception e)
{
tcs.SetException(e);
}
}, null);
return tcs.Task;
}

56
Глава 6. Паттерн TAP
Из-за возможности исключения этот код пришлось несколько ус- ложнить. Если поиск в DNS завершается неудачно, то обращение к
EndGetHostEntry возбуждает исключение. Именно по этой причине в паттерне
IAsyncResult нельзя просто передать результат методу обратного вызова, а необходим метод
End
. Обнаружив исключение, мы должны поместить его в объект
TaskCompletionSource
, что- бы вызывающая программа могла получить его, – в полном соответс- твии с TAP.
На самом деле, количество асинхронных API, построенных по этому образцу, настолько велико, что разработчики .NET включили вспомогательный метод для преобразования их к виду, совместимому с TAP. И мы тоже могли бы воспользоваться этим методом:
Task t = Task.Factory.FromAsync(Dns.BeginGetHostEntry,
Dns.EndGetHostEntry,
hostNameOrAddress,
null);
Этот метод принимает методы
Begin и
End в виде делегатов и внутри устроен очень похоже на наш вариант. Разве что чуть более эффективен.
Холодные и горячие задачи
В библиотеке Task Parallel Library, впервые появившейся в версии
.NET 4.0, было понятие холодной задачи
Task
, которую еще только предстоит запустить, и горячей задачи, которая уже запущена. До сих пор мы имели дело только с горячими задачами.
В спецификации TAP оговорено, что любая задача, возвращенная из метода, должна быть горячей. К счастью, все рассмотренные выше приемы и так создают горячие объекты
Task
. Исключение составляет техника на основе класса
TaskCompletionSource
, к которой по- нятие о горячей или холодной задаче неприменимо. Вы просто долж- ны позаботиться о том, чтобы в какой-то момент сделать объект
Task завершенным самостоятельно.
Предварительная работа
Мы уже знаем, что асинхронный TAP-метод, как и всякий другой, сразу после вызова начинает исполняться в текущем потоке. Разница же в том, что TAP-метод, скорее всего, не завершается в момент


57
возврата. Он быстро возвращает объект-задачу
Task
, которая станет завершенной, когда закончит выполнение.
Тем не менее, какая-то часть метода работает синхронно в текущем потоке. Как мы видели в разделе «Асинхронные методы до поры ис- полняются синхронно», в случае async-метода эта часть содержит по меньшей мере код до первого оператора await
, включая и его опе- ранд.
TAP рекомендует, чтобы синхронно выполняемая часть TAP-ме- тода была сведена к минимуму. В ней можно проверить корректность аргументов и посмотреть, нет ли нужных данных в кэше (что позво- лит избежать длительной операции), но никаких медленных вычис- лений производить не следует. Гибридные методы, в которых сначала выполняется какое-то вычисление, а затем доступ к сети или нечто подобное, вполне допустимы, но рекомендуется перенести вычисле- ние в фоновый поток, вызвав метод
Task.Run
. Вот, например, как мо- жет выглядеть функция, которая закачивает изображение на сайт, но для экономии пропускной способности сети сначала уменьшает его размер:
Image resized = await Task.Run(() => ResizeImage(originalImage));
await UploadImage(resized);
Это существенно в приложении с графическим интерфейсом, но в веб-приложениях не дает практического выигрыша. Тем не менее, мы ожидаем, что метод, следующий рекомендациям TAP, будет воз- вращать управление быстро. Всякий, кто захочет перенести ваш код в приложение с графическим интерфейсом, будет весьма удивлен, увидев, что медленная операция масштабирования изображения вы- полняется синхронно.
Предварительная работ

1   2   3   4   5   6   7   8   9

ГЛАВА 7.
Вспомогательные средства
для асинхронного кода
В дизайн паттерна TAP заложен механизм, позволяющий упростить создание вспомогательных средств для работы с задачами – объекта- ми
Task
. Поскольку любой метод, следующий TAP, возвращает
Task
, любое специализированное поведение, реализованное для одного та- кого метода, можно повторно использовать и в других. В этой главе мы рассмотрим некоторые средства для работы с объектами
Task
, в том числе:
• методы, которые выглядят как TAP-методы, но сами по себе не являются асинхронными, а обладают каким-то полезным специальным поведением;
• комбинаторы, то есть методы, которые порождают из одного объекта
Task другой, в чем-то более полезный;
• средства для отмены асинхронных операций и информирова- ния о ходе их выполнения.
Хотя имеется немало готовых средств такого рода, полезно знать, как реализовать их самостоятельно, – на случай, если понадобится что-то такое, чего в каркасе .NET Framework нет.
Задержка на указанное время
Простейшая длительная операция – это «ничегонеделание» в тече- ние некоторого времени, то есть аналог синхронного метода
Thread.
Sleep
. Вообще-то, и реализовать ее можно, воспользовавшись
Thread.Sleep в сочетании с
Task.Run
:
await Task.Run(() => Thread.Sleep(100));
Но такой простой подход расточителен. Мы захватываем поток только для того, чтобы заблокировать его, а это прямое расточитель-

59
ство ресурсов. Ведь существует способ заставить .NET вызвать ваш код по истечении заданного времени без использования дополни- тельных потоков – класс
System.Threading.Timer
. Поэтому гораздо эффективнее взвести таймер, а затем с помощью класса
TaskComple- tionSource создать объект
Task и сделать его завершенным в момент срабатывания таймера:
private static Task Delay(int millis)
{
TaskCompletionSource tcs = new TaskCompletionSource();
Timer timer = new Timer(_ => tcs.SetResult(null), null, millis,
Timeout.Infi nite);
tcs.Task.ContinueWith(delegate { timer.Dispose(); });
return tcs.Task;
}
Разумеется, это полезная утилита уже предоставляется каркасом.
Она называется
Task.Delay и без сомнения является более мощной, надежной и, вероятно, более эффективной, чем моя версия.
Ожидание завершения
нескольких задач
В разделе «Task и await» выше мы видели, как просто организовать выполнение несколько параллельных асинхронных задач, – нужно запустить их по очереди, а затем ждать завершения каждой. В главе
9 мы узнаем, что необходимо дождаться завершения каждой задачи, иначе можно пропустить исключения.
Для решения этой задачи можно воспользоваться методом
Task.
WhenAll
, который принимает несколько объектов
Task и порождает агрегированную задачу, которая завершается, когда завершены все исходные задачи. Вот простейший вариант метода
WhenAll
(имеется также перегруженный вариант для коллекции универсальных объек- тов
Task
):
Task WhenAll(IEnumerable tasks)
Основное различие между использованием
WhenAll и самосто- ятельным ожиданием нескольких задач, заключается в том, что
WhenAll корректно работает даже в случае исключений. Поэтому старайтесь всегда пользоваться методом
WhenAll
Универсальный вариант
WhenAll возвращает массив, содержащий результаты отдельных поданных на вход задач
Task
. Это сделано ско-
Ожидание завершения нескольких задач