Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Асинхронный шаблон на основе задач можно реализовать тремя способами: с помощью C# и Visual Basic компиляторов в Visual Studio, вручную или с помощью сочетания методов компилятора и вручную. В следующих разделах подробно рассматриваются все методы. Шаблон TAP можно использовать для реализации асинхронных операций, связанных с вычислением, и операций ввода-вывода. В разделе "Рабочие нагрузки" рассматривается каждый тип операции.
Создание методов TAP
Использование компиляторов
Начиная с .NET Framework 4.5 любой метод, который приписывается ключевому слову async (Async в Visual Basic) считается асинхронным методом. Компиляторы C# и Visual Basic выполняют необходимые преобразования для асинхронного реализации метода с помощью TAP. Асинхронный метод должен возвращать System.Threading.Tasks.Task объект или System.Threading.Tasks.Task<TResult> объект. Для последнего текст функции должен возвращать TResultзначение, а компилятор гарантирует, что этот результат доступен через результирующий объект задачи. Аналогичным образом, все исключения, которые остаются необработанными внутри тела метода, передаются в выходную задачу и приводят к завершению результирующей задачи в состоянии TaskStatus.Faulted. Исключение из этого правила заключается в том, что когда OperationCanceledException (или производный тип) остаётся необработанным, в этом случае результирующая задача заканчивается в TaskStatus.Canceled состоянии.
Task.Start и удаление задач
Используйте Start только для задач, явно созданных с помощью конструктора Task, которые все еще находятся в состоянии Created. Общедоступные методы TAP должны возвращать активные задачи, поэтому вызывающим объектам не нужно вызывать Start.
В большинстве кодов TAP не освобождайте задачи. Обычно Task не удерживает неуправляемые ресурсы, и освобождение каждой задачи добавляет лишнюю нагрузку без практической пользы. Удаляется только в том случае, если определенные API или измерения показывают необходимость удаления.
Если вы начинаете фоновую работу, которая продолжается дольше, чем непосредственный вызов, явно определите владение и контролируйте процесс завершения. Дополнительные рекомендации см. в разделе "Сохранение асинхронных методов".
Создание методов TAP вручную
Вы можете реализовать шаблон TAP вручную, чтобы лучше контролировать реализацию. Компилятор использует общедоступную область поверхности, доступную из System.Threading.Tasks пространства имен и вспомогательных типов в System.Runtime.CompilerServices пространстве имен. Чтобы реализовать TAP самостоятельно, создайте TaskCompletionSource<TResult> объект, выполните асинхронную операцию и после завершения, вызовите SetResultметод или SetExceptionSetCanceled метод или Try версию одного из этих методов. При реализации метода TAP вручную необходимо выполнить результирующую задачу при завершении ассоциированной асинхронной операции. Рассмотрим пример.
static class StreamExtensions
{
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object? state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
}
Module StreamExtensions
<Extension()>
Public Function ReadTask(stream As Stream, buffer As Byte(),
offset As Integer, count As Integer,
state As Object) As Task(Of Integer)
Dim tcs As New TaskCompletionSource(Of Integer)()
stream.BeginRead(buffer, offset, count,
Sub(ar)
Try
tcs.SetResult(stream.EndRead(ar))
Catch exc As Exception
tcs.SetException(exc)
End Try
End Sub, state)
Return tcs.Task
End Function
End Module
Гибридный подход
Возможно, вам будет полезно реализовать шаблон TAP вручную, но делегировать основную логику реализации компилятору. Например, может потребоваться использовать гибридный подход, если требуется проверить аргументы за пределами асинхронного метода, созданного компилятором, чтобы исключения могли избежать прямого вызывающего метода, а не предоставляться через System.Threading.Tasks.Task объект:
class Calculator
{
private int value = 0;
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException(nameof(input));
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
// code that uses await goes here
await Task.Delay(1);
return value;
}
}
Class Calculator
Private value As Integer = 0
Public Function MethodAsync(input As String) As Task(Of Integer)
If input Is Nothing Then Throw New ArgumentNullException(NameOf(input))
Return MethodAsyncInternal(input)
End Function
Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)
' code that uses await goes here
Await Task.Delay(1)
Return value
End Function
End Class
Другой случай, когда такое делегирование полезно, заключается в реализации оптимизации быстрого пути и необходимости возвращать кэшированную задачу.
Рабочие нагрузки
Вы можете реализовать асинхронные операции, привязанные к вычислениям, и операции ввода-вывода в виде методов TAP. Однако при публичном предоставлении методов TAP из библиотеки предоставьте их только для рабочих нагрузок, включающих операции ввода-вывода. Эти операции также могут включать вычисления, но они не должны быть чисто вычислительными. Если метод является чисто вычислительным, он предоставляется только в виде синхронной реализации. Затем код, который использует его, может выбрать, следует ли упаковать вызов этого синхронного метода в задачу, чтобы выгрузить работу в другой поток или достичь параллелизма. Если метод привязан к вводу-выводу, он предоставляется только в виде асинхронной реализации.
Задачи, связанные с вычислениями
Класс System.Threading.Tasks.Task хорошо подходит для представления вычислительных интенсивных операций. По умолчанию она использует специальную поддержку в ThreadPool классе, чтобы обеспечить эффективное выполнение. Он также обеспечивает значительный контроль над тем, когда, где и как выполняются асинхронные вычисления.
Создайте задачи, связанные с вычислениями, следующим образом:
В .NET Framework 4.5 и более поздних версиях (включая .NET Core и .NET 5+), используйте статический метод Task.Run как упрощение для TaskFactory.StartNew. Используйте Run для легкого запуска задачи с привязкой к вычислительным ресурсам, предназначенных для пула потоков. Этот метод является предпочтительным механизмом запуска задачи с привязкой к вычислениям. Используйте
StartNewнепосредственно, только если требуется более точное управление задачей.В .NET Framework 4 используйте метод TaskFactory.StartNew. Он принимает делегат (обычно это Action<T> или Func<TResult>) для асинхронного выполнения. При предоставлении делегата Action<T> метод возвращает System.Threading.Tasks.Task объект, представляющий асинхронное выполнение этого делегата. Если вы предоставите делегат Func<TResult>, метод вернет объект System.Threading.Tasks.Task<TResult>. Перегрузки метода StartNew принимают маркер отмены (CancellationToken), параметры создания задач (TaskCreationOptions) и планировщик задач (TaskScheduler). Эти параметры обеспечивают точное управление планированием и выполнением задачи. Экземпляр фабрики, который нацелен на текущий планировщик задач, доступен как статическое свойство (Factory) класса Task. Например:
Task.Factory.StartNew(…).Используйте конструкторы типа
Taskи методStart, если вы хотите создать и запланировать задачу отдельно. Общедоступные методы должны возвращать только запущенные задачи.Используйте перегрузки метода Task.ContinueWith. Этот метод создает новую задачу, которая запланирована при завершении другой задачи. ContinueWith Некоторые перегрузки принимают маркер отмены, параметры продолжения и планировщик задач для более эффективного управления планированием и выполнением задачи продолжения.
Используйте методы TaskFactory.ContinueWhenAll и TaskFactory.ContinueWhenAny. Эти методы создают новую задачу, которая планируется при завершении всех или любой из предоставленных задач. Эти методы также предоставляют перегрузки для управления планированием и выполнением этих задач.
В задачах, связанных с вычислениями, система может предотвратить выполнение запланированной задачи, если она получает запрос на отмену до запуска задачи. Таким образом, если вы предоставляете маркер отмены (CancellationToken объект), этот маркер можно передать в асинхронный код, отслеживающий маркер. Вы также можете предоставить маркер одному из ранее упомянутых методов, таких как StartNew или Run таким образом, что Task среда выполнения также может отслеживать маркер.
Например, рассмотрим асинхронный метод, который отрисовывает изображение. Тело задачи может опрашивать маркер отмены, чтобы код завершился раньше, если запрос на отмену поступает во время отрисовки. Кроме того, если запрос отмены поступает перед началом отрисовки, необходимо предотвратить операцию отрисовки:
internal static Task<Bitmap> RenderAsync(ImageData data, CancellationToken cancellationToken)
{
return Task.Run(() =>
{
var bmp = new Bitmap(data.Width, data.Height);
for (int y = 0; y < data.Height; y++)
{
cancellationToken.ThrowIfCancellationRequested();
for (int x = 0; x < data.Width; x++)
{
// render pixel [x,y] into bmp
}
}
return bmp;
}, cancellationToken);
}
Friend Function RenderAsync(data As ImageData, cancellationToken As CancellationToken) As Task(Of Bitmap)
Return Task.Run(Function()
Dim bmp As New Bitmap(data.Width, data.Height)
For y As Integer = 0 To data.Height - 1
cancellationToken.ThrowIfCancellationRequested()
For x As Integer = 0 To data.Width - 1
' render pixel [x,y] into bmp
Next
Next
Return bmp
End Function, cancellationToken)
End Function
Note
В этом примере используется Bitmap, для которого требуется пакет System.Drawing.Common и поддерживается только в Windows. Шаблон задачи, связанной с вычислительными ресурсами, с использованием Task.Run и CancellationToken, применяется на всех платформах; для систем, отличных от Windows, замените их на кроссплатформенную библиотеку изображений.
Задачи, связанные с вычислениями, заканчиваются состоянием Canceled если выполнено хотя бы одно из следующих условий:
Запрос отмены поступает через CancellationToken объект, который предоставляется в качестве аргумента метода создания (например,
StartNewилиRun) перед переходом задачи в Running состояние.Исключение OperationCanceledException остаётся необработанным в теле такой задачи. Это исключение содержит тот же CancellationToken, который передается в задачу, и этот токен показывает, что была запрошена отмена.
Если другое исключение остается необработанным в теле задачи, задача заканчивается в состоянии Faulted. Любые попытки ждать задачи или получить доступ к её результату приводят к выбрасыванию исключения.
Задачи, ограниченные вводом-выводом
Чтобы создать задачу, которая не должна напрямую использовать поток для всего выполнения, используйте TaskCompletionSource<TResult> тип. Этот тип предоставляет Task свойство, возвращающее связанный Task<TResult> экземпляр. Вы управляете жизненным циклом этой задачи с помощью TaskCompletionSource<TResult> таких методов, как SetResult, SetExceptionSetCanceledи их TrySet варианты.
Предположим, вы хотите создать задачу, которая завершается через указанный период времени. Например, может потребоваться отложить действие в пользовательском интерфейсе. Класс System.Threading.Timer уже предоставляет возможность асинхронного вызова делегата через указанный период времени. С помощью TaskCompletionSource<TResult>, вы можете поместить переднюю часть Task<TResult> на таймер. Рассмотрим пример.
public static Task<DateTimeOffset> Delay(int millisecondsTimeout)
{
TaskCompletionSource<DateTimeOffset>? tcs = null;
Timer? timer = null;
timer = new Timer(delegate
{
timer!.Dispose();
tcs!.TrySetResult(DateTimeOffset.UtcNow);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<DateTimeOffset>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function Delay(millisecondsTimeout As Integer) As Task(Of DateTimeOffset)
Dim tcs As TaskCompletionSource(Of DateTimeOffset) = Nothing
Dim timer As Timer = Nothing
timer = New Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(DateTimeOffset.UtcNow)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of DateTimeOffset)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
Этот Task.Delay метод предоставляется для этой цели. Его можно использовать внутри другого асинхронного метода, например для реализации цикла асинхронного опроса:
public static async Task Poll(Uri url, CancellationToken cancellationToken, IProgress<bool> progress)
{
while (true)
{
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
bool success = false;
try
{
await DownloadStringAsync(url);
success = true;
}
catch { /* ignore errors */ }
progress.Report(success);
}
}
Public Async Function Poll(url As Uri, cancellationToken As CancellationToken,
progress As IProgress(Of Boolean)) As Task
Do While True
Await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken)
Dim success As Boolean = False
Try
Await DownloadStringAsync(url)
success = True
Catch
' ignore errors
End Try
progress.Report(success)
Loop
End Function
У TaskCompletionSource<TResult> класса нет универсального аналога. Однако Task<TResult> выводится из Task, поэтому можно использовать объект TaskCompletionSource<TResult> для методов ввода-вывода, которые просто возвращают задачу. Для этого используйте источник с фиктивным TResult типом (Boolean это хороший вариант по умолчанию, но если вас беспокоит возможность того, что пользователь понизит Task до Task<TResult>, вместо этого вы можете использовать приватный TResult тип). Например, в приведённом ранее примере метод Delay возвращает текущее время вместе с результирующим смещением (Task<DateTimeOffset>). Если такое значение результата не требуется, метод может быть закодирован следующим образом (обратите внимание на изменение возвращаемого типа и изменение аргумента TrySetResultна ):
public static Task<bool> DelaySimple(int millisecondsTimeout)
{
TaskCompletionSource<bool>? tcs = null;
Timer? timer = null;
timer = new Timer(delegate
{
timer!.Dispose();
tcs!.TrySetResult(true);
}, null, Timeout.Infinite, Timeout.Infinite);
tcs = new TaskCompletionSource<bool>(timer);
timer.Change(millisecondsTimeout, Timeout.Infinite);
return tcs.Task;
}
Public Function DelaySimple(millisecondsTimeout As Integer) As Task(Of Boolean)
Dim tcs As TaskCompletionSource(Of Boolean) = Nothing
Dim timer As Timer = Nothing
timer = New Timer(Sub(obj)
timer.Dispose()
tcs.TrySetResult(True)
End Sub, Nothing, Timeout.Infinite, Timeout.Infinite)
tcs = New TaskCompletionSource(Of Boolean)(timer)
timer.Change(millisecondsTimeout, Timeout.Infinite)
Return tcs.Task
End Function
Смешанные задачи с привязкой к вычислительным ресурсам и операции ввода-вывода
Асинхронные методы не ограничиваются только операциями, связанными с вычислениями или операциями ввода-вывода. Они могут представлять собой смесь двух. На самом деле вы часто объединяете несколько асинхронных операций в более крупные смешанные операции. Например, метод RenderAsync в предыдущем примере выполняет требующую больших вычислительных затрат операцию для отрисовки изображения на основе некоторых входных данных imageData. Это imageData может поступать из веб-службы, к которой вы обращаетесь асинхронно.
public static async Task<Bitmap> DownloadDataAndRenderImageAsync(CancellationToken cancellationToken)
{
var imageData = await DownloadImageDataAsync(cancellationToken);
return await RenderAsync(imageData, cancellationToken);
}
Public Async Function DownloadDataAndRenderImageAsync(cancellationToken As CancellationToken) As Task(Of Bitmap)
Dim imageData As ImageData = Await DownloadImageDataAsync(cancellationToken)
Return Await RenderAsync(imageData, cancellationToken)
End Function
Note
В этом примере используется Bitmap, для которого требуется пакет System.Drawing.Common и поддерживается только в Windows. Шаблон цепочки асинхронного скачивания с асинхронной операцией, связанной с вычислением, применяется на всех платформах; замените кроссплатформенную библиотеку изображений для целевых объектов, отличных от Windows.
В этом примере также показано, как один маркер отмены может передаваться через несколько асинхронных операций. Для получения дополнительной информации см. раздел "Отмена задач" в Использование асинхронного шаблона на основе задач.