Поделиться через


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public async Task<int> GetUrlContentLengthAsync()
{
    using 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). Перед ожиданием задачи можно выполнить работу, которая не зависит от stringGetStringAsync.

Обратите внимание на 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 выражение, которое помечает точку, в которой метод не может продолжаться до завершения ожидаемой асинхронной операции. В то же время метод приостановлен, а управление возвращается вызвавшему его. В следующем разделе этого раздела показано, что происходит в точке приостановки.

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

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

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

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

Навигация трассировки асинхронного потока управления

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

  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

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

  • Помеченный асинхронный метод может использовать ожидание для назначения точек приостановки. Оператор 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 не может быть ожидаем, и вызывающий метод с возвращаемым типом void не может перехватывать исключения, которые он может выбрасывать.

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

Дополнительные сведения и примеры см. в статье async return types (C#).

Асинхронные API в программировании среды выполнения Windows имеют один из следующих типов возвращаемых значений, аналогичных задачам:

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

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

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

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

Заголовок Описание
Как выполнять несколько веб-запросов параллельно с использованием async и await (C#) Демонстрирует, как одновременно запускать несколько задач.
Асинхронные возвращаемые типы (C#) Иллюстрирует типы, которые асинхронные методы могут возвращать, и объясняет, когда каждый тип подходит.
Отменяйте задачи, используя токен отмены в качестве механизма сигнализации. Показывает, как добавить следующие функции в асинхронное решение:

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

См. также