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

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

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

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

Добавлен: 10.11.2023

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

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

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

30
Глава 3. Написание асинхронного кода вручную tionContext
. Они, как мы увидим в главе 8, полезны, когда требуется выполнить обратный вызов в конкретном потоке (например, в потоке пользовательского интерфейса).
Сверх того, класс
Task позволяет абстрагировать работу с асинх- ронными операциями. Мы можем воспользоваться композицией для создания вспомогательных средств, которые работают с объектами
Task и предоставляют поведение, полезное во многих ситуациях.
Подробнее об этом речь пойдет в главе 7.
Чем плоха реализация
асинхронности вручную?
Как мы видели, есть много способов реализовать асинхронную прог- рамму. Одни получше, другие похуже. Но надеюсь, вы заметили один общий недостаток. Процедуру необходимо разбить на два метода: запускающий операцию и обратный вызов. Использование в качес- тве обратного вызова анонимного метода или лямбда-выражения от- части сглаживает проблему, зато получается код с большим числом уровней отступа, который трудно читать.
Существует и еще одна проблема. Мы говорили о методах, делаю- щих один асинхронный вызов, но что если таких вызовов несколько?
Или – еще хуже – если асинхронные методы требуется вызывать в цикле? Единственный выход – рекурсивный метод, но воспринима- ется такая конструкция куда сложнее, чем обычный цикл.
private void LookupHostNames(string[] hostNames)
{
LookUpHostNamesHelper(hostNames, 0);
}
private static void LookUpHostNamesHelper(string[] hostNames, int i)
{
Task ipAddressesPromise =
Dns.GetHostAddressesAsync(hostNames[i]);
ipAddressesPromise.ContinueWith(_ =>
{
IPAddress[] ipAddresses = ipAddressesPromise.Result;
// Обработать адрес if (i + 1 < hostNames.Length)
{
LookUpHostNamesHelper(hostNames, i + 1);
}
});
}

31
Переработка примера с использованием...
Фу, гадость какая!
Еще одна проблема, присущая всем рассмотренным способам, – ис- пользование написанного вами асинхронного кода. Если вы написали нечто асинхронно выполняемое и хотите использовать это в другом месте программы, то должны предоставить соответствующий асин- хронный API. Но если использовать асинхронный API трудно и не- удобно, то реализовать его трудно вдвойне. А поскольку асинхронный код «заразен», то иметь дело с асинхронными API придется не только непосредственно вызывающему его модулю, но и тому, который вы- зывает его, и так далее, пока вся программа не превратится в хаос.
Переработка примера
с использованием написанного
вручную асинхронного кода
Напомним, что в примере из предыдущей главы мы обсуждали WPF- приложение, которое не реагировало на действия пользователя, пока скачивало значки веб-сайтов; в это время поток пользовательского интерфейса был блокирован. Теперь посмотрим, как сделать это при- ложение асинхронным, применяя ручную технику.
Прежде всего, нужно найти асинхронную версию метода, кото- рым я пользовался (
WebClient.DownloadData
). Как мы уже видели, в классе
WebClient используется асинхронный событийно-управля- емый паттерн (EAP), поэтому мы можем подписаться на событие об- ратного вызова и начать скачивание.
private void AddAFavicon(string domain)
{
WebClient webClient = new WebClient();
webClient.DownloadDataCompleted += OnWebClientOnDownloadDataCompleted;
webClient.DownloadDataAsync(new Uri(“http://” + domain + “/favicon.ico”));
}
private void OnWebClientOnDownloadDataCompleted(object sender,
DownloadDataCompletedEventArgs args)
{
Image imageControl = MakeImageControl(args.Result);
m_WrapPanel.Children.Add(imageControl);
}
Разумеется, логически неделимую логику придется разбить на два метода. При работе с паттерном EAP я предпочитаю не использовать лямбда-выражение, потому что оно появилось бы в тексте до вызова


32
Глава 3. Написание асинхронного кода вручную метода, в котором производится собственно скачивание, а мне такой код представляется нечитаемым.
Эта версия примера также доступна в сетевом репозитории – в вет- ви manual
. Запустив ее, вы убедитесь, что не только пользователь- ский интерфейс продолжает реагировать на действия пользователя, но и значки появляются постепенно. Но тем самым мы внесли ошиб- ку – поскольку все операции закачки выполняются параллельно, а не последовательно, то порядок следования значков зависит от скорости скачивания, а не от того, в каком порядке они запрашивались. Если вы хотите проверить, хорошо ли понимаете принципы ручного напи- сания асинхронного кода, попробуйте исправить эту ошибку. Одно из возможных решений – находящееся в ветви orderedManual
– осно- вано на преобразовании цикла в рекурсивный метод. Существуют и более эффективные способы.

ГЛАВА 4.
Написание
асинхронных методов
Теперь мы знаем, какими выдающимися достоинствами обладает асинхронный код, но насколько трудно его писать? Самое время поз- накомиться с механизмом async в C# 5.0. Как мы видели в разделе
«Что делает async?» в главе 1, метод, помеченный ключевым словом async
, может содержать ключевое слово await private async void DumpWebPageAsync(string uri)
{
WebClient webClient = new WebClient();
string page = await webClient.DownloadStringTaskAsync(uri);
Console.WriteLine(page);
}
Выражение await в этом примере приводит к преобразованию ме- тода таким образом, что он приостанавливается на время скачивания и возобновляется по его завершении. В результате метод становится асинхронным. В этой главе мы научимся писать такие асинхронные методы.
Преобразование программы
скачивания значков к виду,
использующему async
Сейчас мы изменим программу обозревателя значков сайта, так что- бы воспользоваться механизмом async. Если есть такая возможность, откройте первый вариант примера (из ветви default
) и попробуйте преобразовать его самостоятельно, добавив ключевые слова async и await
, и только потом продолжайте чтение.
Наиболее важен метод
AddAFavicon
, который скачивает значок и отображает его в пользовательском интерфейсе. Мы хотим сделать

34
Глава 4. Написание асинхронных методов этот метод асинхронным, так чтобы поток пользовательского интер- фейса продолжал реагировать на действия пользователя во время скачивания. Первый шаг – пометка метода ключевым словом async
Оно включается в сигнатуру метода точно так же, как, например, сло- во static
Далее мы должны дождаться завершения скачивания, воспользо- вавшись ключевым словом await
. С точки зрения синтаксиса C#, await
– это унарный оператор, такой же как оператор
!
или оператор приведения типа (type). Он располагается слева от выражения и оз- начает, что нужно дождаться завершения асинхронного выполнения этого выражения.
Наконец, вместо вызова метода
DownloadData нужно вызвать его асинхронную версию,
DownloadDataTaskAsync
Метод, помеченный ключевым словом async, автоматически не становится асинхронным. Async-методы лишь упрощают ис- пользование других асинхронных методов. Они начинают испол- няться синхронно, и так происходит до тех пор, пока не встретит- ся вызов асинхронного метода внутри оператора await. В этот момент сам вызывающий метод становится асинхронным. Если же оператор await не встретится, то метод так и будет выпол- няться синхронно до своего завершения.
private async void AddAFavicon(string domain)
{
WebClient webClient = new WebClient();
byte[] bytes = await webClient.DownloadDataTaskAsync(“http://” + domain +
“/favicon.ico”);
Image imageControl = MakeImageControl(bytes);
m_WrapPanel.Children.Add(imageControl);
}
Сравните этот вариант кода с двумя предыдущими. По структу- ре он куда ближе к исходной синхронной версии. Никакого допол- нительного метода нет, добавлено лишь немного кода в теле метода.
Однако ведет он себя, как асинхронная версия, представленная в раз- деле «Переработка примера с использованием написанного вручную асинхронного кода».
Task и await
Рассмотрим подробнее написанное выше выражение await
. Вот как выглядит сигнатура метода
WebClient.DownloadStringTaskAsync
:
Task DownloadStringTaskAsync(string address)


35
Task и await
Тип возвращаемого значения –
Task
. В разделе «Вве- дение в класс Task» я говорил, что класс
Task представляет выпол- няемую операцию, а его подкласс
Task
– операцию, которая в будущем вернет значение типа
T
. Можно считать, что
Task
– это обещание вернуть значение типа
T
по завершении длительной опера- ции.
Оба класса
Task и
Task
могут представлять асинхронные опера- ции, и оба умеют вызывать ваш код по завершении операции. Чтобы воспользоваться этой возможностью вручную, необходимо вызвать метод
ContinueWith
, передав ему код, который должен быть выпол- нен, когда длительная операция завершится. Именно так и поступает оператор await
, чтобы выполнить оставшуюся часть async-метода.
Если применить await к объекту типа
Task
, то мы получим
выражение await, которое само имеет тип
T
. Это означает, что ре- зультат оператора await можно присвоить переменной, которая ис- пользуется далее в методе, что мы и видели в примерах. Однако если await применяется к объекту неуниверсального класса
Task
, то по- лучается предложение await, которое ничему нельзя присвоить (как и результат метода типа void
). Это разумно, потому что класс
Task не обещает вернуть значение в качестве результата, а представляет лишь саму операцию.
await smtpClient.SendMailAsync(mailMessage);
Ничто не мешает разбить выражение await на части и обратиться к
Task напрямую до того, как приступать к ожиданию.
Task myTask = webClient.DownloadStringTaskAsync(uri);
// Здесь можно что-то сделать string page = await myTask;
Важно отчетливо понимать, что при этом происходит. В первой строке вызывается метод
DownloadStringTaskAsync
. Он начинает исполняться синхронно в текущем потоке и, приступив к скачива- нию, возвращает объект
Task
– все еще в текущем потоке.
И лишь когда мы выполняем await для этого объекта, компилятор делает нечто необычное. Это относится и к случаю, когда оператор await непосредственно предшествует вызову асинхронного мето- да.
Длительная операция начинается, как только вызван метод
Down- loadStringTaskAsync
, и это позволяет очень просто организовать одновременное выполнение нескольких асинхронных операций.

36
Глава 4. Написание асинхронных методов
Достаточно просто запустить их по очереди, сохранить все объекты
Task
, а затем ждать их завершения с помощью await
Task fi rstTask =
webClient1.DownloadStringTaskAsync(“http://oreilly.com”);
Task secondTask =
webClient2.DownloadStringTaskAsync(“http://simpletalk.com”);
string fi rstPage = await fi rstTask;
string secondPage = await secondTask;
Такой способ запуска нескольких задач
Task ненадежен, если задача может возбуждать исключения. Если обе операции воз- будят исключения, то первый await передаст исключение в вы- зывающую программу, а до ожидания задачи secondTask дело не дойдет вовсе. Возбужденное ей исключение останется неза- меченным и в зависимости от версии и настроек .NET может быть проигнорировано или повторно возбуждено в другом пото- ке, где его никто не ожидает, что приведет к завершению про- цесса. В главе 7 мы рассмотрим более правильные способы па- раллельного исполнения нескольких задач.
Тип значения, возвращаемого
асинхронным методом
Метод, помеченный ключевым словом async
, может возвращать зна- чения трех типов:
• void

Task

Task
, где
T
– некоторый тип.
Никакие другие типы не допускаются, потому что в общем случае исполнение асинхронного метода не завершается в момент возвра- та управления. Как правило, асинхронный метод ждет завершения длительной операции, то есть управление-то он возвращает сразу, но результат в этот момент еще не получен и, стало быть, недоступен вы- зывающей программе.
Я провожу различие между типом возвращаемого методом зна- чения (например,
Task) и типом результата, который нужен вызывающей программе (в данном случае string).
В обычных, не асинхронных, методах тип возвращаемого значе- ния совпадает с типом результата, тогда как в асинхронных ме- тодах они различны – и это очень существенно.


37
Async, сигнатуры методов и интерфейсы
Очевидно, что в асинхронном случае void
– вполне разумный тип возвращаемого значения. Метод, описанный как async void
, можно рассматривать как операцию вида «запустил и забыл». Вызывающая программа не ждет результата и не может узнать, когда и как опера- ция завершается. Использовать тип void следует, когда вы уверены, что вызывающей программе безразлично, когда завершится операция и завершится ли она успешно. Чаще всего методы типа async void употребляются на границе между асинхронным и прочим кодом, на- пример, обработчик события пользовательского интерфейса должен возвращать void
Async-методы, возвращающие тип
Task
, позволяют вызывающей программе ждать завершения асинхронной операции и распростра- няют исключения, имевшие место при ее выполнении. Если значе- ние результата несущественно, то метод типа async Task предпочти- тельнее метода типа async void
, потому что вызывающая программа получает возможность узнать о завершении операции, что упрощает упорядочение задач и обработку исключений.
Наконец, async-методы, возвращающие тип
Task
, например
Task
, используются, когда асинхронная операция возвра- щает некий результат.
Async, сигнатуры методов
и интерфейсы
Ключевое слово async указывается в объявлении метода, как public или static
. Однако спецификатор async
не считается частью сиг- натуры метода, когда речь заходит о переопределении виртуальных методов, реализации интерфейса или вызове.
Единственное назначение ключевого слова async
– изменить спо- соб компиляции соответствующего метода, на взаимодействие с ок- ружением оно не оказывает никакого влияния. Поэтому в правилах, действующих в отношении переопределения методов и реализации интерфейсов, слово async полностью игнорируется.
class BaseClass
{
public virtual async Task AlexsMethod()
{
}
}

38
Глава 4. Написание асинхронных методов class SubClass : BaseClass
{
// Переопределяет метод базового класса AlexsMethod public override Task AlexsMethod()
{
}
}
В объявлениях методов интерфейса слово async запрещено прос- то потому, что в этом нет необходимости. Если в интерфейсе объяв- лен метод, возвращающий тип
Task
, то в реализующем интерфейс классе этот метод может быть помечен словом async
, а может быть и не помечен – на усмотрение программиста.
Предложение return
в асинхронных методах
В асинхронном методе предложение return ведет себя особым об- разом. Напомним, что в обычном, не асинхронном, методе правила, действующие отношении return
, зависят от типа, возвращаемого методом.
Методы типа void
Предложение return должно содержать только return;
и мо- жет быть опущено.
Методы, возвращающие тип
T
Предложение return должно содержать выражение типа
T
(например, return 5+x;
); при этом любой путь исполнения метода должен завершаться предложением return
В методах, помеченных ключевым словом async
, действуют дру- гие правила.
Методы типа void и методы, возвращающие тип
Task
Предложение return должно содержать только return;
и мо- жет быть опущено.
Методы, возвращающие тип
Task
Предложение return должно содержать выражение типа
T
; при этом любой путь исполнения метода должен завершаться предложением return
В async-методах тип возвращаемого значения отличается от типа выражения, указанного в предложении return
. Выполняемое компи-


39
Асинхронные методы «заразны»
лятором преобразование можно рассматривать как обертывание воз- вращенного значения объектом
Task
до передачи его вызывающей программе. Конечно, в действительности объект
Task
создается сразу же, но результат в него записывается позже, когда длительная операция завершится.
Асинхронные методы «заразны»
Как мы видели, чтобы воспользоваться объектом
Task
, возвращен- ным каким-то асинхронным API, проще всего дождаться его с помо- щью оператора await в async-методе. Но при таком подходе и ваш метод будет возвращать
Task
(как правило). Чтобы получить выиг- рыш от асинхронности, программа, вызывающая ваш метод, не долж- на блокироваться в ожидании завершении задачи
Task
, и, следова- тельно, вызов вашего метода, вероятно, также будет производиться с помощью await
Ниже приведен пример вспомогательного метода, который под- считывает количество символов на веб-странице и асинхронно воз- вращает результат.
private async Task GetPageSizeAsync(string url)
{
WebClient webClient = new WebClient();
string page = await webClient.DownloadStringTaskAsync(url);
return page.Length;
}
Чтобы им воспользоваться, я должен написать еще один async-ме- тод, который возвращает результат также асинхронно:
private async Task FindLargestWebPage(string[] urls)
{
string largest = null;
int largestSize = 0;
foreach (string url in urls)
{
int size = await GetPageSizeAsync(url);
if (size > largestSize)
{
size = largestSize;
largest = url;
}
}
return largest;
}

40
Глава 4. Написание асинхронных методов
В итоге получается цепочка async-методов, каждый из которых ждет следующего с помощью оператора await
. Асинхронная модель программирования «заразна», она с легкостью распространяется по всему коду продукта. Но я не считаю это серьезной проблемой – ведь писать асинхроннные методы очень легко.
Асинхронные анонимные
делегаты и лямбда-выражения
Асинхронными могут быть как обычные именованные методы, так и обе формы анонимных методов. Вот как сделать асинхронным ано- нимный делегат:
Func> getNumberAsync = async delegate { return 3; };
А вот так – лямбда-выражение:
Func> getWordAsync = async () => “hello”;
При этом действуют те же правила, что для обычных асинхронных методов. Асинхронные анонимные методы можно применять для со- кращения размера кода или для формирования замыканий – точно так же, как не асинхронные.

1   2   3   4   5   6   7   8   9