Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
È 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
StartNewdirettamente 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
Taske il metodoStartse 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,
StartNewoRun) 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à.