Асинхронный шаблон на основе задач (TAP) в .NET: введение и обзор

В .NET асинхронный шаблон на основе задач — это рекомендуемый шаблон асинхронного проектирования для новой разработки. Он основан на Task типах и Task<TResult> типах в System.Threading.Tasks пространстве имен, которые используются для представления асинхронных операций.

Именование, параметры и возвращаемые типы

TAP использует один метод для представления инициализации и завершения асинхронной операции. Это отличается от шаблона модели асинхронного программирования (APM или IAsyncResult) и асинхронного шаблона, основанного на событиях (EAP). Для APM требуется метод Begin и End. Для EAP требуется метод с суффиксом Async, а также одно или несколько событий, типов делегата обработчика событий и производные от EventArg типы. В асинхронных методах TAP после имени операции используется суффикс Async — для методов, возвращающих типы с поддержкой ожидания, например Task, Task<TResult>, ValueTask и ValueTask<TResult>. Синхронные операции Get, возвращающие Task<String>, могут называться GetAsync. Если вы добавляете к классу, который уже содержит имя метода EAP с суффиксом Async, метод TAP, используйте вместо него суффикс TaskAsync. Например, класс уже содержит метод GetAsync, используйте имя GetTaskAsync. Если метод запускает асинхронную операцию, но не возвращает ожидаемый тип, его имя должно начинаться с Begin, Start или другого глагола, который указывает на то, что этот метод не возвращает или не выдает результат операции.  

Метод TAP возвращает System.Threading.Tasks.Task или System.Threading.Tasks.Task<TResult> в зависимости от того, возвращает ли соответствующий синхронный метод значение void или тип TResult.

Параметры метода TAP должны соответствовать параметрам его синхронного аналога и предоставляться в том же порядке. Однако параметры out и ref исключены из этого правила, их следует избегать полностью. Все данные, которые были бы возвращены параметром out или ref, должны вместо этого возвращаться как часть результата TResult, возвращаемого Task<TResult>, и должны использовать кортеж или пользовательскую структуру данных, чтобы вместить несколько значений. Попробуйте добавить параметр CancellationToken, даже если в аналогичном синхронном методе TAP этот параметр не применяется.

К методам, которые предназначены исключительно для создания, обработки или сочетания задач (где асинхронная природа метода очевидна из имени метода или имени типа, к которому относится метод), эта схема именования не применяется. Такие методы часто называются методами объединения. К таким методам относятся WhenAll и WhenAny, которые рассматриваются в разделе Использование внутренних блоков объединения задач статьи Использование асинхронного шаблона, основанного на задачах.

Примеры отличий синтаксиса TAP от синтаксиса, используемого в устаревших асинхронных моделях, таких как асинхронная модель программирования (APM) и асинхронная модель на основе событий (EAP), вы найдете в статье Шаблоны асинхронного программирования.

Инициализация асинхронной операции

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

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

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

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

Исключения

Асинхронный метод должен вызывать исключение, которое должно создаваться вызовом асинхронного метода только в ответ на ошибку использования. Ошибки использования никогда не должны происходить в рабочем коде. Например, если передача пустой ссылки (Nothing в Visual Basic) в виде одного из аргументов метода вызывает ошибочное состояние (как правило, представляется исключением ArgumentNullException), можно изменить вызывающий код, чтобы убедиться в том, что пустая ссылка никогда не передается. Для всех остальных ошибок исключения, возникающие во время исполнения асинхронного метода, должны относиться к возвращаемой задаче, даже если асинхронный метод выполняется синхронно перед возвращением задачи. Как правило, задача содержит не более одного исключения. Однако если задача представляет множественные операции (например, WhenAll), с одной задачей может быть связано несколько исключений.

Целевая среда

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

Вызывающий метода TAP может приостановить работу, ожидая завершения метода TAP путем синхронного ожидания результирующей задачи, или выполнять дополнительный код, продолжающий работу после завершения асинхронной операции. Автор кода продолжения имеет контроль над местом исполнения кода. Можно создать код продолжения явным образом с помощью методов в классе Task (например, ContinueWith) или неявно путем поддержки языка на основе продолжений (например, await в C#, Await в Visual Basic, AwaitValue в F#).

Состояние задачи

Класс Task обеспечивает жизненный цикл для асинхронных операций, и этот цикл представлен перечислением TaskStatus. Для поддержки угловых вариантов типов, производных от Task и Task<TResult>, а также для поддержки разделения конструкции от планирования, Task класс предоставляет Start метод. Задачи, созданные открытыми конструкторами Task, называются холодными задачами, так как они начинают свой жизненный цикл в незапланированном состоянии Created, а их планирование осуществляется только тогда, когда в этих экземплярах вызывается метод Start.

Все другие задачи начинают свой жизненный цикл в активном состоянии, то есть асинхронные операции, которые они представляют, уже были инициированы и их статус задачи — это значение перечисления, отличное от TaskStatus.Created. Необходимо активировать все задачи, возвращаемые методами TAP. Если внутри метода TAP используется конструктор задачи, создающий экземпляр возвращаемой задачи, метод TAP перед ее возвращением должен вызвать метод Start для объекта Task. Объекты-получатели метода TAP могут с уверенностью допускать, что возвращаемая задача активна, и не пытаться вызвать метод Start для любого объекта Task, который возвращается из метода TAP. Вызов метода Start в активной задаче приводит к исключению InvalidOperationException.

Отмена (необязательно)

В TAP отмена является необязательной как для асинхронной реализации метода, так и для асинхронных объектов-получателей метода. Если операция позволяет выполнить отмену, она предоставляет перезагрузку асинхронного метода, принимающую токен отмены (экземпляр CancellationToken). По правилам этот параметр называется cancellationToken.

public Task ReadAsync(byte [] buffer, int offset, int count,
                      CancellationToken cancellationToken)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          cancellationToken As CancellationToken) _
                          As Task

Асинхронная операция отслеживает этот токен на наличие запросов на отмену. Если операция получает запрос на отмену, системой может быть принято решение об удовлетворении запроса и отмене операции. Если запрос на отмену приводит к преждевременному завершению работы, метод TAP возвращает задачу, которая завершается в состоянии Canceled; в этом случае отсутствует результат и исключение не создается. Состояние Canceled считается конечным состоянием для задачи, наравне с состояниями Faulted и RanToCompletion. Таким образом, если задача находится в состоянии Canceled, ее свойство IsCompleted возвращает значение true. Если задача завершается в состоянии Canceled, любые продолжения, зарегистрированные для задачи, планируются или исполняются, если только не был выбран параметр отказа от продолжения, такой как NotOnCanceled. Любой код, который асинхронно ожидает отмененной задачи с использованием языковых возможностей, продолжает выполняться, но получает исключение OperationCanceledException или производное от него исключение. Код, который блокируется во время синхронного ожидания задачи с использованием методов Wait и WaitAll также продолжает выполняться с исключением.

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

В асинхронных методах, для которых возможность выполнить отмену является обязательным условием, не обязательно предоставлять перегрузку, не принимающую маркер отмены. Для методов, которые не могут быть отменены, не нужно предоставлять перегрузки, принимающие токен отмены; это позволяет указать вызывающему объекту, можно ли в действительности отменить целевой метод. Код объекта-получателя, в котором отмена является нежелательной, может вызвать метод, который принимает CancellationToken и предоставляет None в качестве значения аргумента. None функционально эквивалентен значению по умолчанию CancellationToken.

Отчет о ходе выполнения (необязательно)

Для некоторых асинхронных операций имеет смысл предоставлять уведомления о ходе выполнения; обычно уведомления используются для обновления информации о выполнении асинхронной операции в пользовательском интерфейсе.

В TAP ход выполнения контролируется в интерфейсе IProgress<T>, который передается в асинхронный метод в качестве параметра, который обычно называется progress. Предоставление интерфейса для контроля за ходом выполнения при вызове асинхронного метода позволяет избежать состояний гонки, возникающих в результате неправильного использования (т. е. когда обработчики событий, неправильно зарегистрированные после начала операции, пропускают обновления). Еще более важно то, что интерфейс выполнения поддерживает различные реализации хода выполнения в соответствии с определением в коде-потребителе. Например, потребляющий код может заботиться только о последнем обновлении хода выполнения, или может потребоваться буферизировать все обновления, или может потребоваться вызвать действие для каждого обновления, или может потребоваться контролировать, маршалируется ли вызов в конкретный поток. Все эти варианты могут быть реализованы с использованием иной реализации интерфейса, настроенной в соответствии с требованиями потребителя. Как и в случае с отменой, реализации TAP должны предоставлять параметр IProgress<T>, только если API поддерживает уведомления о ходе выполнения.

Например, если метод ReadAsync, обсуждаемый ранее в этой статье, может информировать о ходе выполнения в виде считанного количества байтов, обратный вызов хода выполнения может быть представлен интерфейсом IProgress<T>:

public Task ReadAsync(byte[] buffer, int offset, int count,
                      IProgress<long> progress)
Public Function ReadAsync(buffer() As Byte, offset As Integer,
                          count As Integer,
                          progress As IProgress(Of Long)) As Task

Если метод FindFilesAsync возвращает список всех файлов, соответствующих указанному шаблону поиска, обратный вызов хода выполнения может предоставить оценку процента выполненной работы и текущий частичный результат. Он может предоставить эти сведения в виде кортежа:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
            string pattern,
            IProgress<Tuple<double,
            ReadOnlyCollection<List<FileInfo>>>> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of Tuple(Of Double, ReadOnlyCollection(Of List(Of FileInfo))))) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

или с помощью типа данных, соответствующего данному API:

public Task<ReadOnlyCollection<FileInfo>> FindFilesAsync(
    string pattern,
    IProgress<FindFilesProgressInfo> progress)
Public Function FindFilesAsync(pattern As String,
                               progress As IProgress(Of FindFilesProgressInfo)) _
                               As Task(Of ReadOnlyCollection(Of FileInfo))

В последнем случае к специальному типу данных добавляется суффикс ProgressInfo.

Если реализации TAP предоставляют перегрузки, принимающие параметр progress, они должны разрешать значение null для аргумента. В этом случае о ходе выполнения не сообщается. В реализации TAP необходимо синхронно сообщать информацию о ходе выполнения объекту Progress<T>, что позволит асинхронному методу быстро предоставлять сведения о ходе выполнения. Это также позволит объекту — получателю этой информации определять, как и где лучше обрабатывать ее. Например, экземпляр хода выполнения может маршалировать обратные вызовы и инициировать события для захваченного контекста синхронизации.

Реализации IProgressT<>

.NET предоставляет класс Progress<T>, который реализует IProgress<T>. Класс Progress<T>объявляется следующим образом:

public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T>? ProgressChanged;  
}  

Экземпляр Progress<T> предоставляет событие ProgressChanged, которое вызывается каждый раз, когда асинхронная операция сообщает об обновлении хода выполнения. Событие ProgressChanged вызывается для объекта SynchronizationContext, который был захвачен при создании экземпляра Progress<T>. Если контекст синхронизации не был доступен, то используется контекст по умолчанию, предназначенный для пула потоков. Обработчики могут быть зарегистрированы при помощи этого события. Один обработчик также может предоставляться конструктору Progress<T> для удобства. Он работает точно так же, как обработчик события ProgressChanged. Обновления хода выполнения вызываются асинхронно, чтобы избежать задержки асинхронной операции при выполнении обработчиков событий. В другой реализации IProgress<T> может применяться другая семантика.

Выбор перегрузок для предоставления

Если реализация TAP использует необязательные параметры CancellationToken и IProgress<T>, потенциально может потребоваться до четырех перегрузок:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
public Task MethodNameAsync(…, IProgress<T> progress);
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken cancellationToken) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Однако многие реализации TAP не поддерживают ни отмену, ни просмотр информации о ходе выполнения, поэтому им требуется единственный метод:

public Task MethodNameAsync(…);  
Public MethodNameAsync(…) As Task  

Если реализация TAP поддерживает либо отмену, либо просмотр хода выполнения, но не одновременно, то она может предоставлять две перегрузки:

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, CancellationToken cancellationToken);  
  
// … or …  
  
public Task MethodNameAsync(…);  
public Task MethodNameAsync(…, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken) As Task  
  
' … or …  
  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, progress As IProgress(Of T)) As Task  

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

public Task MethodNameAsync(…);  
public Task MethodNameAsync(…,
    CancellationToken cancellationToken, IProgress<T> progress);  
Public MethodNameAsync(…) As Task  
Public MethodNameAsync(…, cancellationToken As CancellationToken,
                       progress As IProgress(Of T)) As Task  

Чтобы возместить два отсутствующих промежуточных сочетания, разработчик может передать значение None или значение по умолчанию CancellationToken для параметра cancellationToken и null для параметра progress.

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

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

Заголовок Описание
Модели асинхронного программирования Представляет три шаблона для выполнения асинхронных операций: асинхронную модель на основе задач (TAP), асинхронную модель программирования (APM) и асинхронную модель на основе событий (EAP).
Реализация асинхронной модели на основе задач Описывает три способа реализации асинхронной модели на основе задач (TAP): с помощью компиляторов C# и Visual Basic в Visual Studio, вручную или путем сочетания этих методов.
Consuming the Task-based Asynchronous Pattern Описывает, как можно использовать задачи и обратные вызовы для реализации неблокирующего ожидания.
Взаимодействие с другими асинхронными шаблонами и типами Описывает, как использовать асинхронную модель на основе задач (TAP) для реализации асинхронной модели программирования (APM) и асинхронной модели на основе событий (EAP).