Implementazione del modello asincrono basato su attività

È possibile implementare il modello asincrono basato su attività (TAP) in tre modi: usando I compilatori C# e Visual Basic in Visual Studio, manualmente o tramite una combinazione del compilatore e dei metodi manuali. Le sezioni seguenti illustrano in dettaglio ogni metodo. È possibile usare il pattern TAP per implementare operazioni asincrone sia legate al calcolo che all'I/O. La sezione Carichi di lavoro illustra ogni tipo di operazione.

Generazione di metodi TAP

Uso dei compilatori

A partire da .NET Framework 4.5, qualsiasi metodo attribuito con la parola chiave async (Async in Visual Basic) viene considerato un metodo asincrono. I compilatori C# e Visual Basic eseguono le trasformazioni necessarie per implementare il metodo in modo asincrono usando TAP. Un metodo asincrono deve restituire un System.Threading.Tasks.Task oggetto o .System.Threading.Tasks.Task<TResult> Per quest'ultimo, il corpo della funzione deve restituire un TResult, e il compilatore garantisce che questo risultato venga reso disponibile tramite l'oggetto di attività risultante. Analogamente, tutte le eccezioni non gestite all'interno del corpo del metodo vengono eseguite operazioni di marshalling nel task di output, causando che l'attività risultante finisca nello stato TaskStatus.Faulted. L'eccezione a questa regola è quando un OperationCanceledException (o un tipo derivato) non viene gestito, nel qual caso l'attività TaskStatus.Canceled risultante termina nello stato .

Attività.Avvio e terminazione attività

Usare Start solo per le attività create esplicitamente con un costruttore Task che sono ancora nello stato Created. I metodi TAP pubblici devono restituire attività in esecuzione, quindi i chiamanti non devono chiamare Start.

Nella maggior parte dei codici TAP, non eliminare i task. Un Task non detiene risorse non gestite nel caso tipico, e l'eliminazione di ogni operazione aggiunge sovraccarico senza beneficio pratico. Elimina solo quando specifiche API o misurazioni mostrano una necessità.

Nel caso in cui si avvia un lavoro in background che persiste oltre il contesto di chiamata immediato, è consigliabile mantenere esplicita la proprietà e monitorare il completamento. Per altre indicazioni, vedere Mantenere attivi i metodi asincroni.

Generazione manuale di metodi TAP

È possibile implementare manualmente il modello TAP per un migliore controllo sull'implementazione. Il compilatore si basa sull'area di superficie pubblica esposta dallo spazio dei nomi System.Threading.Tasks e sui tipi di supporto nello spazio dei nomi System.Runtime.CompilerServices. Per implementare il TAP manualmente, creare un oggetto TaskCompletionSource<TResult>, eseguire l'operazione asincrona e, al termine, chiamare il metodo SetResult, SetException o SetCanceled, o la versione Try di uno di questi metodi. Quando si implementa manualmente un metodo TAP, è necessario completare l'attività risultante al termine dell'operazione asincrona rappresentata. Per esempio:

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

Approccio ibrido

Potrebbe risultare utile implementare manualmente il modello TAP, ma delegare la logica di base per l'implementazione al compilatore. Ad esempio, è possibile usare l'approccio ibrido quando si desidera verificare gli argomenti all'esterno di un metodo asincrono generato dal compilatore, in modo che le eccezioni possano propagare verso il chiamante diretto del metodo anziché essere gestite tramite l'oggetto 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

Un altro caso in cui tale delega è utile è quando si implementa l'ottimizzazione fast-path e si vuole restituire un'attività memorizzata nella cache.

Carichi di lavoro

È possibile implementare operazioni asincrone sia limitate dal calcolo sia limitate dall'I/O come metodi TAP. Tuttavia, quando si espongono i metodi TAP pubblicamente da una libreria, specificarli solo per i carichi di lavoro che coinvolgono operazioni associate a I/O. Queste operazioni possono anche comportare calcoli, ma non devono essere puramente computazionali. Se un metodo è puramente associato al calcolo, esporlo solo come implementazione sincrona. Il codice che lo utilizza può quindi scegliere se incapsulare una chiamata del metodo sincrono in un'attività per delegare il lavoro a un altro thread o per ottenere il parallelismo. Se un metodo è associato a I/O, esporlo solo come implementazione asincrona.

Attività associate al calcolo

La System.Threading.Tasks.Task classe funziona bene per rappresentare operazioni a elevato utilizzo di calcolo. Per impostazione predefinita, sfrutta il supporto speciale all'interno della ThreadPool classe per garantire un'esecuzione efficiente. Fornisce anche un controllo significativo su quando, dove e come vengono eseguiti i calcoli asincroni.

Generare attività associate a calcolo nei modi seguenti:

  • In .NET Framework 4.5 e versioni successive (inclusi .NET Core e .NET 5+), usare il metodo statico Task.Run come collegamento a TaskFactory.StartNew. Usare Run per avviare facilmente un'attività associata a calcolo destinata al pool di thread. Questo metodo è il meccanismo preferito per l'avvio di un'attività associata a calcolo. Usare StartNew direttamente solo quando si desidera un controllo più granulare sull'attività.

  • In .NET Framework 4 usare il metodo TaskFactory.StartNew. Accetta un delegato (in genere un Action<T> o un Func<TResult>) da eseguire in modo asincrono. Se si specifica un Action<T> delegato, il metodo restituisce un System.Threading.Tasks.Task oggetto che rappresenta l'esecuzione asincrona di tale delegato. Se si specifica un Func<TResult> delegato, il metodo restituisce un System.Threading.Tasks.Task<TResult> oggetto . Gli overload del metodo StartNew accettano un token di annullamento (CancellationToken), opzioni per la creazione di attività (TaskCreationOptions) e un task scheduler (TaskScheduler). Questi parametri forniscono un controllo granulare sulla pianificazione e l'esecuzione dell'attività. Un'istanza di factory che si rivolge allo scheduler del compito corrente è disponibile come proprietà statica (Factory) della classe Task. Ad esempio: Task.Factory.StartNew(…).

  • Usare i costruttori del tipo Task e il metodo Start se si desidera generare e pianificare l'attività separatamente. I metodi pubblici devono restituire solo attività già avviate.

  • Usare i sovraccarichi della funzione Task.ContinueWith. Questo metodo crea una nuova attività pianificata al completamento di un'altra attività. Alcuni sovraccarichi ContinueWith accettano un token di annullamento, opzioni di continuazione e uno scheduler dei task, offrendo un miglior controllo sulla pianificazione e l'esecuzione del task di continuazione.

  • Usa i metodi TaskFactory.ContinueWhenAll e TaskFactory.ContinueWhenAny. Questi metodi creano una nuova attività pianificata che viene avviata quando tutte o alcune delle attività di un gruppo specificato vengono completate. Questi metodi forniscono anche delle sovraccarichi per controllare la pianificazione e l'esecuzione di questi compiti.

Nelle attività associate a calcolo, il sistema può impedire l'esecuzione di un'attività pianificata se riceve una richiesta di annullamento prima di iniziare a eseguire l'attività. Di conseguenza, se si fornisce un token di annullamento (CancellationToken oggetto ), è possibile passare tale token al codice asincrono che monitora il token. È anche possibile fornire il token a uno dei metodi indicati in precedenza, StartNew ad esempio o Run in modo che il Task runtime possa anche monitorare il token.

Si consideri, ad esempio, un metodo asincrono che esegue il rendering di un'immagine. Il corpo dell'attività può interrogare il token di annullamento in modo che il codice esca in anticipo se arriva una richiesta di annullamento durante il rendering. Inoltre, se la richiesta di annullamento arriva prima dell'avvio del rendering, si vuole impedire l'operazione di rendering:

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

Questo esempio usa , che richiede il pacchetto /> Task.Run insieme a CancellationToken, si applica a tutte le piattaforme; sostituire con una libreria di immagini multipiattaforma per ambienti non Windows.

Le attività associate al calcolo terminano in uno Canceled stato se almeno una delle condizioni seguenti è vera:

  • Una richiesta di annullamento arriva tramite l'oggetto CancellationToken , fornito come argomento per il metodo di creazione (ad esempio, StartNew o Run) prima che l'attività passi allo Running stato.

  • Un'eccezione OperationCanceledException non viene gestita all'interno del corpo di un task di questo tipo. Tale eccezione contiene lo stesso CancellationToken passato al task, e tale token indica che è richiesto l'annullamento.

Se un'altra eccezione non viene gestita all'interno del corpo dell'attività, l'attività termina nello stato Faulted. Qualsiasi tentativo di attendere l'attività o di accedere al suo risultato causa la generazione di un'eccezione.

Attività vincolate dall'I/O

Per creare un'attività che non deve usare direttamente un thread per l'intera esecuzione, usare il TaskCompletionSource<TResult> tipo . Questo tipo espone una Task proprietà che restituisce un'istanza associata Task<TResult> . È possibile controllare il ciclo di vita di questa attività usando TaskCompletionSource<TResult> metodi come SetResult, SetExceptionSetCanceled, e le relative TrySet varianti.

Si supponga di voler creare un'attività che venga completata dopo un periodo di tempo specificato. Ad esempio, potrebbe essere necessario ritardare un'attività nell'interfaccia utente. La System.Threading.Timer classe offre già la possibilità di richiamare in modo asincrono un delegato dopo un periodo di tempo specificato. Usando TaskCompletionSource<TResult>, è possibile posizionare l'interfaccia Task<TResult> sul timer. Per esempio:

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

Il Task.Delay metodo viene fornito a questo scopo. È possibile usarlo all'interno di un altro metodo asincrono, ad esempio, per implementare un ciclo di polling asincrono:

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

La TaskCompletionSource<TResult> classe non ha una controparte non generica. Tuttavia, Task<TResult> deriva da Task, in modo da poter usare l'oggetto generico TaskCompletionSource<TResult> per i metodi associati a I/O che restituiscono semplicemente un'attività. A tale scopo, usa una sorgente con un TResult dummy (Boolean è una buona opzione predefinita, ma se sei preoccupato che l'utente del Task esegua un downcasting su un Task<TResult>, puoi invece usare un tipo privato TResult). Ad esempio, il Delay metodo nell'esempio precedente restituisce l'ora corrente insieme all'offset risultante (Task<DateTimeOffset>). Se tale valore di risultato non è necessario, il metodo potrebbe invece essere codificato come segue (si noti la modifica del tipo restituito e la modifica dell'argomento 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

Attività miste dipendenti dal calcolo e dall'I/O

I metodi asincroni non sono limitati solo alle operazioni associate a calcolo o di I/O. Possono rappresentare una miscela dei due. In effetti, spesso si combinano più operazioni asincrone in operazioni miste più grandi. Ad esempio, il RenderAsync in un esempio precedente esegue un'operazione intensiva dal punto di vista computazionale per generare un'immagine in base a dei dati di input imageData. Questo imageData può provenire da un servizio Web a cui si accede in modo asincrono:

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

Questo esempio usa , che richiede il pacchetto />

Questo esempio illustra anche come un singolo token di annullamento può essere sottoposto a threading tramite più operazioni asincrone. Per altre informazioni, vedere la sezione relativa all'utilizzo dell'annullamento in Uso del modello asincrono basato su attività.

Vedere anche