Condividi tramite


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 async parola chiave (Async in Visual Basic) viene considerato un metodo asincrono e i compilatori C# e Visual Basic eseguono le trasformazioni necessarie per implementare il metodo in modo asincrono tramite 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 .

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:

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;
}
<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

Approccio ibrido

Può 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 eseguire l'escape al chiamante diretto del metodo anziché essere esposte tramite l'oggetto System.Threading.Tasks.Task :

public Task<int> MethodAsync(string input)
{
    if (input == null) throw new ArgumentNullException("input");
    return MethodAsyncInternal(input);
}

private async Task<int> MethodAsyncInternal(string input)
{

   // code that uses await goes here

   return value;
}
Public Function MethodAsync(input As String) As Task(Of Integer)
    If input Is Nothing Then Throw New ArgumentNullException("input")

    Return MethodAsyncInternal(input)
End Function

Private Async Function MethodAsyncInternal(input As String) As Task(Of Integer)

    ' code that uses await goes here

    return value
End Function

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 legate al calcolo o all'I/O come metodi TAP. Tuttavia, quando i metodi TAP vengono esposti pubblicamente da una libreria, devono essere forniti solo per i carichi di lavoro che coinvolgono operazioni associate a I/O (possono anche comportare calcoli, ma non devono essere puramente computazionali). Se un metodo è puramente associato al calcolo, deve essere esposto 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 parallelismo. E se un metodo è associato a I/O, deve essere esposto solo come implementazione asincrona.

Attività associate al calcolo

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

È possibile generare attività associate al 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. È possibile usare Run per avviare facilmente un'attività associata a calcolo destinata al pool di thread. Si tratta del 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, utilizzare il metodo TaskFactory.StartNew, che accetta un delegato, solitamente un oggetto Action<T> o 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 . I sovraccarichi del metodo StartNew accettano un token di annullamento (CancellationToken), opzioni di creazione delle attività (TaskCreationOptions) e integrazione con un gestore di task (TaskScheduler), tutti elementi che forniscono un controllo granulare sulla pianificazione e sull'esecuzione dell'attività. Un'istanza factory destinata allo schedulatore di attività 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ò verificare il token di annullamento in modo che il codice possa terminare anticipatamente se arriva una richiesta di annullamento durante il rendering. Inoltre, se la richiesta di annullamento arriva prima dell'avvio del rendering, è necessario impedire l'operazione di rendering:

internal 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

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'attività di questo tipo; questa eccezione contiene lo stesso CancellationToken passato all'attività, e tale token indica che è stato richiesto l'annullamento.

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

Attività vincolate dall'I/O

Per creare un'attività che non deve essere supportata direttamente da un thread per l'intera esecuzione, usare il TaskCompletionSource<TResult> tipo . Questo tipo espone una Task proprietà che restituisce un'istanza associata Task<TResult> . Il ciclo di vita di questa attività è controllato da TaskCompletionSource<TResult> metodi come SetResult, SetException, SetCancelede le relative TrySet varianti.

Si supponga di voler creare un'attività che verrà completata dopo un periodo di tempo specificato. Ad esempio, è possibile 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 e usando TaskCompletionSource<TResult> è possibile inserire un Task<TResult> elemento anteriore sul timer, ad 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 ed è 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, è possibile usare una sorgente con un fittizio TResult (Boolean è una buona scelta predefinita, ma se si è preoccupati che l'utente di Task lo trasmetta in modo limitato a Task<TResult>, è possibile usare invece 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> Delay(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 Delay(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 I/O, ma possono rappresentare una combinazione dei due. Infatti, più operazioni asincrone vengono spesso combinate in operazioni miste più grandi. Ad esempio, il metodo RenderAsync in un esempio precedente ha eseguito un'operazione computazionalmente intensiva per eseguire il rendering di un'immagine in base ad alcuni input imageData. Questo imageData può provenire da un servizio Web a cui si accede in modo asincrono:

public 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

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