Асинхронная модель программирования

Асинхронное программирование позволяет избежать появления узких мест производительности и увеличить общую скорость реагирования приложения. Однако традиционные методы создания асинхронных приложений могут оказаться сложными, как в плане написания кода, так и в плане отладки и обслуживания.

C# поддерживает упрощенный подход, асинхронное программирование, которое использует асинхронную поддержку в среде выполнения .NET. Компилятор выполняет сложную работу, которую раньше делал разработчик, при этом логическая структура кода приложения похожа на синхронный код. То есть можно пользоваться всеми преимуществами асинхронного программирования с гораздо меньшими, чем обычно, трудозатратами.

В этом разделе рассматриваются области и методы использования асинхронного программирования, а также приводятся ссылки на вспомогательные разделы с дополнительными сведениями и примерами.

Асинхрон улучшает скорость реагирования

Асинхронность необходимо использовать при наличии потенциально блокирующих работу действий, например при осуществлении доступа к Интернету. Доступ к веб-ресурсу иногда осуществляется медленно или с задержкой. Если такое действие блокируется в синхронном процессе, все приложение вынуждено ожидать. В асинхронном процессе приложение может перейти к следующей операции, не зависящей от веб-ресурса, до завершения блокирующей задачи.

В следующей таблице показаны стандартные области, в которых асинхронное программирование повышает скорость реагирования. Перечисленные API-интерфейсы платформы .NET и среды выполнения Windows содержат методы, поддерживающие асинхронное программирование.

Область приложения Типы .NET с методами Async Типы среды выполнения Windows с методами Async
Веб-доступ HttpClient Windows.Web.Http.HttpClient
SyndicationClient
Работа с файлами JsonSerializer
StreamReader
StreamWriter
XmlReader
XmlWriter
StorageFile
Работа с образами MediaCapture
BitmapEncoder
BitmapDecoder
Программирование с использованием WCF Синхронные и асинхронные операции

Асинхронность особенно полезна в приложениях, которые обращаются к потоку пользовательского интерфейса, поскольку все связанные с пользовательским интерфейсом действия обычно используют один поток. В синхронном приложении блокировка одного процесса приводит к блокировке всех процессов. Приложение перестает отвечать, и это выглядит как сбой, а не как ожидание.

При использовании асинхронных методов приложение продолжает реагировать на действия в пользовательском интерфейсе. Например, можно изменить размер окна или свернуть его, либо закрыть приложение, если не требуется ждать завершения работы.

Асинхронный подход добавляет эквивалент автоматической передачи в список параметров, выбранных при создании асинхронных операций. То есть разработчик может пользоваться всеми преимуществами традиционного асинхронного программирования, прикладывая гораздо меньше усилий.

Асинхронные методы легко записывать

В C# основой асинхронного программирования. являются ключевые слова async и await. Они позволяют использовать ресурсы платформы .NET Framework, .NET Core или среды выполнения Windows для создания асинхронных методов, и это почти так же просто, как создавать синхронные методы. Асинхронные методы, которые определяются с помощью ключевого слова async, называются методами async.

Ниже приводится пример асинхронного метода. Почти все элементы кода должны быть вам знакомы.

Полный пример Windows Presentation Foundation (WPF), доступный для загрузки, см. в статье Асинхронное программирование с использованием ключевых слов async и await в C#.

public async Task<int> GetUrlContentLengthAsync()
{
    var client = new HttpClient();

    Task<string> getStringTask =
        client.GetStringAsync("https://learn.microsoft.com/dotnet");

    DoIndependentWork();

    string contents = await getStringTask;

    return contents.Length;
}

void DoIndependentWork()
{
    Console.WriteLine("Working...");
}

Из предыдущего примера вы можете узнать несколько полезных методик. Начните с сигнатуры метода. Она включает модификатор async. Типом возвращаемого значения является Task<int> (дополнительные параметры см. в разделе "Типы возвращаемых значений"). Имя метода заканчивается на Async. В теле метода GetStringAsync возвращает Task<string>. Это означает, что когда вы ожидаете (await) задачу, то получаете string (contents). Прежде чем ожидать задачу, можно сделать работу, которая не зависит от string из GetStringAsync.

Обратите внимание на оператор await. Он приостанавливает GetUrlContentLengthAsync.

  • GetUrlContentLengthAsync не может продолжить выполнение до завершения getStringTask.
  • На это время управление возвращается вызывающему объекту метода GetUrlContentLengthAsync.
  • Управление возобновляется после завершения getStringTask.
  • Оператор await извлекает результат string из getStringTask.

Оператор return указывает целочисленный результат. Все методы, ожидающие GetUrlContentLengthAsync, получают значение длины.

Если метод GetUrlContentLengthAsync не выполняет никакие операции между вызовом метода GetStringAsync и его завершением, можно упростить код, описав вызов и ожидание с помощью следующего простого оператора.

string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");

Далее поясняется, почему код предыдущего примера является асинхронным методом:

  • Сигнатура метода включает модификатор async.

  • Имя асинхронного метода, как правило, оканчивается суффиксом Async.

  • Возвращаемое значение имеет один из следующих типов:

    • Task<TResult>, если метод включает оператор return с операндом типа TResult.
    • Task, если метод не имеет оператора Return или имеет оператор Return без операнда.
    • void, если вы создаете асинхронный обработчик событий.
    • Любой GetAwaiter другой тип, имеющий метод.

    Дополнительные сведения см. в описании типов возвращаемого значения и параметров.

  • Метод обычно содержит по меньшей мере одно выражение await, отмечающее точку, в которой метод не может продолжить выполнение до завершения асинхронной операции. На это время метод приостанавливается и управление возвращается к вызывающему объекту метода. В следующем подразделе показано, что происходит в точке приостановки.

В асинхронных методах с помощью соответствующих ключевых слов и типов указывается, что требуется сделать, а компилятор выполняет остальные задачи, в том числе отслеживает обязательные действия, когда управление возвращается в точку await в приостановленном методе. Некоторые программные процессы, такие как циклы и обработка исключений, сложно добавлять в традиционный асинхронный код. В асинхронном методе эти элементы пишутся почти так же, как в синхронном решении, что очень удобно.

Дополнительные сведения об асинхронности в предыдущих версиях платформы .NET Framework см. в статье Библиотека параллельных задач и традиционное асинхронное программирование .NET Framework.

Что происходит в асинхронном методе

В асинхронном программировании важнее всего понимать, как поток управления перемещается из метода в метод. Следующая схема описывает этот процесс.

Trace navigation of async control flow

Числа на схеме соответствуют следующим шагам, которые запускаются, когда вызывающий метод вызывает асинхронный метод.

  1. Вызывающий метод вызывает и ожидает асинхронный метод GetUrlContentLengthAsync.

  2. GetUrlContentLengthAsync создает экземпляр HttpClient и вызывает асинхронный метод GetStringAsync, чтобы загрузить содержимое веб-сайта в виде строки.

  3. В GetStringAsync происходит событие, которое приостанавливает ход выполнения. Например, методу необходимо подождать завершения загрузки или произошло другое блокирующее действие. Чтобы избежать блокировки ресурсов, GetStringAsync передает управление вызывающему объекту GetUrlContentLengthAsync.

    GetStringAsync возвращает Task<TResult>, где TResult — строка, а GetUrlContentLengthAsync присваивает задачу переменной getStringTask. Задача представляет собой непрерывный процесс для вызова GetStringAsync с обязательством создать фактическое значение строки, когда работа будет завершена.

  4. Поскольку значение из процесса getStringTask еще не получено, метод GetUrlContentLengthAsync может перейти к другим операциям, не зависящим от конечного результатаGetStringAsync. Эти операции представлены вызовом синхронного метода DoIndependentWork.

  5. DoIndependentWork — это синхронный метод, который выполняет свой код и возвращает управление вызывающему объекту.

  6. Метод GetUrlContentLengthAsync выполнил все операции, для которых не требуется результат процесса getStringTask. Далее метод GetUrlContentLengthAsync должен вычислить длину загруженной строки и возвратить ее, но не может этого сделать, пока нет строки.

    Поэтому GetUrlContentLengthAsync использует оператор await, чтобы приостановить свою работу и передать управление методу, вызвавшему GetUrlContentLengthAsync. GetUrlContentLengthAsync возвращает вызывающему объекту Task<int>. Задача представляет собой обещание создать целочисленный результат, являющийся длиной загруженной строки.

    Примечание.

    Если метод GetStringAsync (и, следовательно, процесс getStringTask) завершается прежде, чем этого дождется GetUrlContentLengthAsync, управление остается у метода GetUrlContentLengthAsync. На приостановку метода GetUrlContentLengthAsync и последующий возврат к нему были бы потрачены лишние ресурсы, если вызываемый асинхронный процесс getStringTask уже завершен и GetUrlContentLengthAsync не нужно ждать окончательного результата.

    В вызывающем методе сохраняется шаблон обработки. Вызывающий объект может выполнять другие операции, не зависящие от результата GetUrlContentLengthAsync, во время ожидания этого результата, или сразу ожидать результата. Вызывающий метод ожидает GetUrlContentLengthAsync, а GetUrlContentLengthAsync ожидает GetStringAsync.

  7. GetStringAsync завершается и создает строковый результат. Вызов возвращает строковый результат в метод GetStringAsync, но не так, как, возможно, ожидалось. (Помните, что метод уже возвратил задачу на шаге 3.) Вместо этого строковый результат хранится в задаче getStringTask, которая представляет собой завершение метода. Оператор await извлекает результат из getStringTask. Оператор присваивания назначает извлеченный результат contents.

  8. Если GetUrlContentLengthAsync содержит строковый результат, метод может вычислить длину строки. Затем работа GetUrlContentLengthAsync также завершена, и ожидающий обработчик событий может возобновить работу. В полном примере в конце этого раздела видно, что обработчик событий извлекает значение длины и выводит результат. Если вы недавно занимаетесь асинхронным программированием, рекомендуем обратить внимание на различия между синхронным и асинхронным поведением. Синхронный метод возвращает управление, когда его работа завершается (шаг 5), тогда как асинхронный метод возвращает значение задачи, когда его работа приостанавливается (шаги 3 и 6). Когда асинхронный метод в конечном счете завершает работу, задача помечается как завершенная и результат, при его наличии, сохраняется в задаче.

Асинхронные методы API

Где же найти методы для асинхронного программирования (такие как GetStringAsync)? Платформа .NET Framework 4.5 и более поздние версии, а также .NET Core содержат большое количество элементов, совместимых с async и await. Они содержат суффикс Async в имени элемента и возвращают тип Task или Task<TResult>. Например, класс System.IO.Stream имеет такие методы, как CopyToAsync, ReadAsync и WriteAsync, наряду с синхронными методами CopyTo, Read и Write.

Среда выполнения Windows также содержит множество методов, которые можно использовать в сочетании с async и await в приложениях Windows. См. дополнительные сведения о потоковом и асинхронном программировании для разработки UWP, асинхронном программировании (приложение Магазина Windows), а также краткое руководство по вызову асинхронных API в C# или Visual Basic, если вы используете предыдущие версии среды выполнения Windows.

Потоки

Асинхронные методы используются для неблокирующих операций. Выражение await в асинхронном методе не блокирует текущий поток на время выполнения ожидаемой задачи. Вместо этого выражение регистрирует остальную часть метода как продолжение и возвращает управление вызывающему объекту асинхронного метода.

Ключевые слова async и await не вызывают создания дополнительных потоков. Асинхронные методы не требуют многопоточности, поскольку асинхронный метод не выполняется в собственном потоке. Метод выполняется в текущем контексте синхронизации и использует время в потоке, только когда метод активен. Метод Task.Run можно применять для перемещения операций, использующих ресурсы ЦП, в фоновый поток, однако фоновый поток не имеет смысла применять для процесса, который просто ждет результата.

Асинхронный подход к асинхронному программированию практически по всем параметрам имеет преимущество перед другими подходами. В частности, такой подход эффективнее, чем использование класса BackgroundWorker для привязанных к вводу-выводу операций, так как используется более простой код и вам не нужно специально предотвращать состояние гонки. В сочетании с методом Task.Run асинхронное программирование лучше BackgroundWorker для операций, использующих ресурсы процессора, так как оно отделяет координацию выполнения кода от действий, которые Task.Run перемещает в пул потоков.

async и await

Если с помощью модификатора async указать, что метод является асинхронным, у вас появятся следующие две возможности.

  • Асинхронный метод сможет использовать await для обозначения точек приостановки. Оператор await сообщает компилятору, что асинхронный метод не может выполняться после этой точки до завершения ожидаемого асинхронного процесса. На это время управление возвращается вызывающему объекту асинхронного метода.

    Приостановка асинхронного метода на выражении await не считается выходом из метода, и блоки finally не выполняются.

  • Сам обозначенный асинхронный метод может ожидаться вызывающими его методами.

Асинхронный метод обычно содержит одно или несколько вхождений оператора await, но отсутствие выражений await не вызывает ошибок компилятора. Если асинхронный метод не использует оператор await для обозначения точки приостановки, метод выполняется как синхронный независимо от наличия модификатора async. При компиляции таких методов выдается предупреждение.

async и await являются контекстными ключевыми словами. Дополнительные сведения и примеры см. в следующих разделах:

Возвращаемые типы и параметры

В результате работы асинхронного метода обычно возвращается Task или Task<TResult>. Внутри асинхронного метода оператор await применяется к задаче, возвращаемой из вызова другого асинхронного метода.

В качестве возвращаемого типа указывается Task<TResult>, если метод содержит оператор return, который задает операнд типа TResult.

В качестве возвращаемого типа используется Task, если метод не содержит операторов return или содержит оператор return, который не возвращает операнд.

Можно также указать любой другой тип возвращаемого GetAwaiter значения, при условии, что тип включает метод. Пример такого типа — ValueTask<TResult>. Он доступен в NuGet-пакете System.Threading.Tasks.Extension.

В следующем примере показано объявление и вызов метода, который возвращает Task<TResult> или Task:

async Task<int> GetTaskOfTResultAsync()
{
    int hours = 0;
    await Task.Delay(0);

    return hours;
}


Task<int> returnedTaskTResult = GetTaskOfTResultAsync();
int intResult = await returnedTaskTResult;
// Single line
// int intResult = await GetTaskOfTResultAsync();

async Task GetTaskAsync()
{
    await Task.Delay(0);
    // No return statement needed
}

Task returnedTask = GetTaskAsync();
await returnedTask;
// Single line
await GetTaskAsync();

Каждая возвращенная задача представляет выполняющуюся работу. Задача инкапсулирует информацию о состоянии асинхронного процесса и, в итоге, либо конечный результат процесса, либо исключение, созданное процессом в случае его сбоя.

Асинхронный метод может иметь тип возвращаемого значения void. Возвращаемый тип в основном используется для определения обработчиков событий, где требуется возвращать тип void. Асинхронные обработчики событий часто служат в качестве отправной точки для асинхронных программ.

Асинхронный метод, который имеет тип возвращаемого значения void, невозможно ожидать методом await. Вызывающий объект не может перехватывать исключения, которые выдает такой метод.

Асинхронный метод не может объявлять параметры in, ref или out, но может вызывать методы с этими параметрами. Аналогичным образом, асинхронный метод не может возвращать значение по ссылке, несмотря на то, что он может вызывать методы с возвращаемыми значениями ref.

Дополнительные сведения и примеры см. в статье Типы возвращаемых значений асинхронных операций (C#).

При программировании в среде выполнения Windows асинхронные API-интерфейсы имеют один из следующих возвращаемых типов, которые похожи на задачи.

Соглашение об именовании

По соглашению имена методов, возвращающих обычно поддерживающие ожидание типы (например, Task, Task<T>, ValueTask и ValueTask<T>), должны заканчиваться на Async. Имена методов, которые запускают асинхронную операцию, но не возвращают поддерживающий ожидание тип, не должны заканчиваться на Async, но могут начинаться с Begin, Start или другой команды, предполагающей, что метод не возвращает и не выдает результат операции.

Соглашение можно игнорировать в тех случаях, когда событие, базовый класс или контракт интерфейса предлагает другое имя. Например, не следует переименовывать общие обработчики событий, такие как OnButtonClick.

Связанные статьи (Visual Studio)

Заголовок Description
Практическое руководство. Параллельное выполнение нескольких веб-запросов с использованием Async и Await (C#) Иллюстрирует, как запустить несколько задач одновременно.
Типы возвращаемых значений асинхронных операций (C#) Иллюстрирует типы, которые могут возвращать асинхронные методы, и поясняет, когда следует использовать каждый из этих типов.
Отмените задачи с маркером отмены в качестве механизма сигнализации. Иллюстрирует добавление следующих функциональных возможностей в асинхронное решение:

- Отмена списка задач (C#)
- Отмена задач после определенного периода времени (C#)
- Обработка асинхронных задач по мере завершения (C#)
Использование метода async для доступа к файлам (C#) Иллюстрирует преимущества использования асинхронности и ожидания для доступа к файлам.
Асинхронный шаблон, основанный на задачах (TAP) Описывает асинхронную модель, шаблон основан на типах Task и Task<TResult>.
Видеоролики об async на канале Channel 9 Предоставляет ссылки на различные видеоролики об асинхронном программировании.

См. также