Het asynchrone patroon op basis van taken implementeren

U kunt het op taken gebaseerde asynchrone patroon (TAP) op drie manieren implementeren: met behulp van de C# en Visual Basic compilers in Visual Studio, handmatig of via een combinatie van de compiler en handmatige methoden. In de volgende secties wordt elke methode in detail besproken. U kunt het TAP-patroon gebruiken om asynchrone bewerkingen te implementeren die afhankelijk zijn van rekenkracht en I/O-afhankelijke bewerkingen. In de sectie Workloads wordt elk type bewerking besproken.

TAP-methoden genereren

De compilers gebruiken

Vanaf .NET Framework 4.5 wordt elke methode die is toegeschreven aan het trefwoord async (Async in Visual Basic) beschouwd als een asynchrone methode. De C# en Visual Basic compilers voeren de benodigde transformaties uit om de methode asynchroon te implementeren met behulp van TAP. Een asynchrone methode moet een System.Threading.Tasks.Task of een System.Threading.Tasks.Task<TResult> object retourneren. Voor deze laatste moet de hoofdtekst van de functie een TResult teruggeven, en de compiler zorgt ervoor dat dit resultaat beschikbaar wordt gesteld via het resulterende taakobject. Eventuele uitzonderingen die niet worden verwerkt in de hoofdtekst van de methode, worden ook aan de uitvoertaak toegewezen en zorgen ervoor dat de resulterende taak eindigt in de TaskStatus.Faulted status. De uitzondering op deze regel is wanneer een OperationCanceledException (of afgeleid type) niet wordt verwerkt. In dat geval eindigt de resulterende taak in de TaskStatus.Canceled status.

Task.Start en taak verwijderen

Alleen gebruiken Start voor taken die expliciet zijn gemaakt met een Task constructor die nog steeds de Created status heeft. Openbare TAP-methoden moeten actieve taken retourneren, zodat oproepers Start niet hoeven aan te roepen.

In de meeste gevallen van TAP-code worden taken niet opgeheven. A Task bevat geen onbeheerde resources in het typische geval en het verwijderen van elke taak voegt overhead toe zonder praktische voordelen. Verwijder alleen wanneer specifieke API's of metingen een behoefte vertonen.

Als u achtergrondwerk start dat het directe oproeppad overleeft, zorg ervoor dat u het eigendom duidelijk vastlegt en de voltooiing volgt. Zie Asynchrone methoden actief houden voor meer informatie.

TAP-methoden handmatig genereren

U kunt het TAP-patroon handmatig implementeren voor betere controle over de implementatie. De compiler is afhankelijk van het openbare oppervlak dat wordt weergegeven vanuit de System.Threading.Tasks naamruimte en ondersteunende typen in de System.Runtime.CompilerServices naamruimte. Als u de TAP zelf wilt implementeren, maakt u een TaskCompletionSource<TResult> object, voert u de asynchrone bewerking uit en wanneer deze is voltooid, roept u de SetResultmethode SetExceptionof SetCanceled de Try versie van een van deze methoden aan. Wanneer u handmatig een TAP-methode implementeert, moet u de resulterende taak voltooien wanneer de vertegenwoordigde asynchrone bewerking is voltooid. Voorbeeld:

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

Hybride benadering

Het kan handig zijn om het TAP-patroon handmatig te implementeren, maar de kernlogica voor de implementatie naar de compiler delegeren. U kunt bijvoorbeeld de hybride benadering gebruiken als u argumenten buiten een door een compiler gegenereerde asynchrone methode wilt controleren, zodat uitzonderingen kunnen ontsnappen aan de directe aanroeper van de methode in plaats van beschikbaar te worden gemaakt via het System.Threading.Tasks.Task object:

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

Een ander geval waarin dergelijke delegatie nuttig is, is wanneer u snelle padoptimalisatie implementeert en een taak in de cache wilt retourneren.

Werklast

U kunt zowel reken- als I/O-gebonden asynchrone bewerkingen implementeren als TAP-methoden. Wanneer u TAP-methoden echter openbaar beschikbaar maakt vanuit een bibliotheek, geeft u deze alleen op voor workloads waarvoor I/O-afhankelijke bewerkingen zijn vereist. Deze bewerkingen kunnen ook betrekking hebben op berekeningen, maar ze mogen niet uitsluitend rekenkracht hebben. Als een methode uitsluitend afhankelijk is van rekenkracht, maakt u deze alleen beschikbaar als een synchrone implementatie. De code die deze gebruikt, kan vervolgens kiezen of u een aanroep van die synchrone methode in een taak wilt verpakken om het werk naar een andere thread te offloaden of om parallellisme te bereiken. Als een methode I/O-gebonden is, maakt u deze alleen beschikbaar als asynchrone implementatie.

Berekeningsgebonden taken

De System.Threading.Tasks.Task klasse werkt goed voor het weergeven van rekenintensieve bewerkingen. Standaard wordt gebruikgemaakt van speciale ondersteuning binnen de ThreadPool klasse om efficiënte uitvoering te bieden. Het biedt ook aanzienlijke controle over wanneer, waar en hoe asynchrone berekeningen worden uitgevoerd.

Genereer rekentaken op de volgende manieren:

  • Gebruik in .NET Framework 4.5 en hoger (inclusief .NET Core en .NET 5+) de statische Task.Run methode als snelkoppeling naar TaskFactory.StartNew. Gebruik Run dit om eenvoudig een rekengebonden taak te starten die is gericht op de threadpool. Deze methode is het voorkeursmechanisme voor het starten van een rekengebonden taak. Gebruik StartNew deze functie alleen rechtstreeks als u meer gedetailleerde controle over de taak wilt.

  • Gebruik in .NET Framework 4 de methode TaskFactory.StartNew. Het accepteert een gemachtigde (meestal een Action<T> of a Func<TResult>) om asynchroon uit te voeren. Als u een Action<T> gemachtigde opgeeft, retourneert de methode een System.Threading.Tasks.Task object dat de asynchrone uitvoering van die gemachtigde vertegenwoordigt. Als u een Func<TResult> gemachtigde opgeeft, retourneert de methode een System.Threading.Tasks.Task<TResult> object. Overbelastingen van de StartNew methode accepteren een annuleringstoken (CancellationToken), opties voor het maken van taken (TaskCreationOptions) en een taakplanner (TaskScheduler). Deze parameters bieden nauwkeurige controle over de planning en uitvoering van de taak. Een factory-exemplaar dat is gericht op de huidige taakplanner, is beschikbaar als een statische eigenschap (Factory) van de Task klasse. Bijvoorbeeld: Task.Factory.StartNew(…).

  • Gebruik de constructors van het Task type en de Start methode als u de taak afzonderlijk wilt genereren en plannen. Openbare methoden mogen alleen taken retourneren die al zijn gestart.

  • Gebruik de overbelasting van de Task.ContinueWith methode. Met deze methode maakt u een nieuwe taak die wordt gepland wanneer een andere taak is voltooid. Sommige overbelastingen ContinueWith accepteren een annuleringstoken, vervolgopties en een taakplanner voor betere controle over de planning en uitvoering van de vervolgtaak.

  • Gebruik de TaskFactory.ContinueWhenAll en TaskFactory.ContinueWhenAny methoden. Met deze methoden maakt u een nieuwe taak die wordt gepland wanneer alle of een van een opgegeven set taken is voltooid. Deze methoden bieden ook overbelastingen om de planning en uitvoering van deze taken te beheren.

Bij rekentaken kan het systeem de uitvoering van een geplande taak voorkomen als er een annuleringsaanvraag wordt ontvangen voordat de taak wordt uitgevoerd. Als u een annuleringstoken (CancellationToken object) opgeeft, kunt u dat token doorgeven aan de asynchrone code die het token bewaakt. U kunt het token ook opgeven op een van de eerder genoemde methoden, zoals StartNew of Run zodat de Task runtime het token ook kan bewaken.

Denk bijvoorbeeld aan een asynchrone methode waarmee een afbeelding wordt weergegeven. De taak zelf kan het annuleringstoken peilen, zodat de code vroegtijdig wordt afgesloten als er een annuleringsaanvraag binnenkomt tijdens het renderen. Als de annuleringsaanvraag binnenkomt voordat de rendering begint, wilt u bovendien voorkomen dat de renderingbewerking wordt uitgevoerd:

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

Opmerking

In dit voorbeeld wordt gebruikgemaakt van Bitmap, waarvoor het System.Drawing.Common-pakket is vereist en alleen wordt ondersteund op Windows. Het berekeningsgebonden taakpatroon( met behulp van Task.Run met een CancellationToken), is van toepassing op alle platforms; vervang een platformoverschrijdende imagingbibliotheek voor niet-Windows doelen.

Berekeningsgebonden taken eindigen in een Canceled status als ten minste een van de volgende voorwaarden waar is:

  • Er wordt een annuleringsaanvraag ingediend via het CancellationToken object, dat wordt opgegeven als argument voor de aanmaakmethode (bijvoorbeeld StartNew of Run) voordat de taak overgaat naar de Running status.

  • Een OperationCanceledException uitzondering wordt niet verwerkt binnen de hoofdtekst van een dergelijke taak. Deze uitzondering bevat hetzelfde CancellationToken dat wordt doorgegeven aan de taak en dat token laat zien dat annulering is aangevraagd.

Als een andere uitzondering niet wordt verwerkt binnen de hoofdtekst van de taak, eindigt de taak in de Faulted status. Pogingen om op de taak te wachten of toegang te krijgen tot het resultaat, zorgt ervoor dat er een uitzondering wordt gegenereerd.

I/O-gebonden taken

Als u een taak wilt maken die niet rechtstreeks een thread moet gebruiken voor de volledige uitvoering, gebruikt u het TaskCompletionSource<TResult> type. Met dit type wordt een Task eigenschap weergegeven die een gekoppeld Task<TResult> exemplaar retourneert. U bepaalt de levenscyclus van deze taak met behulp van TaskCompletionSource<TResult> methoden zoals SetResult, SetExceptionen SetCanceledhun TrySet varianten.

Stel dat u een taak wilt maken die na een opgegeven periode wordt voltooid. U kunt bijvoorbeeld een activiteit in de gebruikersinterface vertragen. De System.Threading.Timer klasse biedt al de mogelijkheid om een gemachtigde asynchroon aan te roepen na een opgegeven periode. Door gebruik te maken van TaskCompletionSource<TResult>, kunt u een Task<TResult> vooraan op de timer plaatsen. Voorbeeld:

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

Hiervoor wordt de Task.Delay methode opgegeven. U kunt deze in een andere asynchrone methode gebruiken, bijvoorbeeld om een asynchrone polling-lus te implementeren:

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

De TaskCompletionSource<TResult> klasse heeft geen niet-generieke tegenhanger. Task<TResult> Is echter afgeleid vanTask, zodat u het algemene TaskCompletionSource<TResult> object kunt gebruiken voor I/O-gebonden methoden die gewoon een taak retourneren. Gebruik hiervoor een bron met een dummy TResult (Boolean is een goede standaardkeuze, maar als u zich zorgen maakt dat de gebruiker van de Task het downcast naar een Task<TResult>, kunt u in plaats daarvan een privé TResult-type gebruiken). De methode in het vorige voorbeeld retourneert bijvoorbeeld Delay de huidige tijd, samen met de resulterende offset (Task<DateTimeOffset>). Als een dergelijke resultaatwaarde niet nodig is, kan de methode in plaats daarvan als volgt worden gecodeerd (let op de wijziging van het retourtype en de wijziging van het argument in 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

Gemengde reken- en I/O-afhankelijke taken

Asynchrone methoden zijn niet beperkt tot alleen reken- of I/O-gebonden bewerkingen. Ze kunnen een combinatie van de twee vertegenwoordigen. In feite combineert u vaak meerdere asynchrone bewerkingen tot grotere gemengde bewerkingen. De methode in een vorig voorbeeld voert bijvoorbeeld RenderAsync een rekenintensieve bewerking uit om een afbeelding weer te geven op basis van invoer imageData. Dit imageData kan afkomstig zijn van een webservice waartoe u asynchroon toegang hebt:

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

Opmerking

In dit voorbeeld wordt gebruikgemaakt van Bitmap, waarvoor het System.Drawing.Common-pakket is vereist en alleen wordt ondersteund op Windows. Het patroon van het koppelen van een asynchrone download met een asynchrone rekenbewerking is van toepassing op alle platforms; vervang een platformoverschrijdende imagingbibliotheek voor niet-Windows doelen.

In dit voorbeeld wordt ook gedemonstreerd hoe één annuleringstoken door meerdere asynchrone bewerkingen kan worden doorlopen. Zie de sectie annuleringsgebruik in Het gebruik van het Asynchrone patroon op basis van taken voor meer informatie.

Zie ook