Espressioni di attività
Questo articolo descrive il supporto in F# per le espressioni di attività, simili alle espressioni asincrone , ma che consentono di creare direttamente attività .NET. Analogamente alle espressioni asincrone, le espressioni di attività eseguono codice in modo asincrono, ovvero senza bloccare l'esecuzione di altre operazioni.
Il codice asincrono viene in genere creato usando espressioni asincrone. L'uso delle espressioni di attività è preferibile quando si interagisce ampiamente con le librerie .NET che creano o usano attività .NET. Le espressioni di attività possono anche migliorare le prestazioni e l'esperienza di debug. Tuttavia, le espressioni di attività presentano alcune limitazioni, descritte più avanti nell'articolo.
Sintassi
task { expression }
Nella sintassi precedente il calcolo rappresentato da expression
viene configurato per l'esecuzione come attività .NET. L'attività viene avviata immediatamente dopo l'esecuzione di questo codice e viene eseguita nel thread corrente fino a quando non viene eseguita la prima operazione asincrona, ad esempio una sospensione asincrona, un I/O asincrono asincrono o un'altra operazione asincrona primitiva. Il tipo dell'espressione è Task<'T>
, dove 'T
è il tipo restituito dall'espressione quando viene usata la return
parola chiave .
Associazione con let!
In un'espressione di attività alcune espressioni e operazioni sono sincrone e alcune sono asincrone. Quando si attende il risultato di un'operazione asincrona, anziché un'associazione normale let
, si usa let!
. L'effetto di let!
è consentire all'esecuzione di continuare su altri calcoli o thread durante l'esecuzione del calcolo. Al termine della restituzione del lato destro dell'associazione let!
, il resto dell'attività riprende l'esecuzione.
Il codice seguente illustra la differenza tra let
e let!
. La riga di codice che usa let
crea semplicemente un'attività come oggetto che è possibile attendere in un secondo momento usando, ad esempio, task.Wait()
o task.Result
. La riga di codice che usa let!
avvia l'attività e ne attende il risultato.
// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int) = stream.ReadAsync(buffer, offset, count, cancellationToken)
Le espressioni F# task { }
possono attendere i tipi di operazioni asincrone seguenti:
- Attività .NET Task<TResult> e .Task
- Attività di valore ValueTask<TResult> .NET e non generico ValueTask.
- Calcoli asincroni
Async<T>
F# . - Qualsiasi oggetto che segue il modello "GetAwaiter" specificato in F# RFC FS-1097.
Espressioni return
All'interno delle espressioni di attività viene return expr
usato per restituire il risultato di un'attività.
Espressioni return!
All'interno delle espressioni di attività viene return! expr
usato per restituire il risultato di un'altra attività. Equivale a usare let!
e quindi restituire immediatamente il risultato.
Flusso di controllo
Le espressioni di attività possono includere i costrutti for .. in .. do
del flusso di controllo , , while .. do
try .. finally ..
try .. with ..
, if .. then .. else
, e .if .. then ..
Questi possono a loro volta includere ulteriori costrutti di attività, ad eccezione dei with
gestori e finally
che vengono eseguiti in modo sincrono. Se è necessaria un'associazione asincrona try .. finally ..
, usare un'associazione use
in combinazione con un oggetto di tipo IAsyncDisposable
.
use
associazioni e use!
All'interno di espressioni di attività, use
le associazioni possono essere associati ai valori di tipo IDisposable o IAsyncDisposable. Per quest'ultimo, l'operazione di pulizia dello smaltimento viene eseguita in modo asincrono.
Oltre a let!
, è possibile usare use!
per eseguire associazioni asincrone. La differenza tra let!
e use!
corrisponde alla differenza tra let
e .use
Per use!
, l'oggetto viene eliminato alla chiusura dell'ambito corrente. Si noti che in F# 6 use!
non consente l'inizializzazione di un valore su Null, anche se use
lo fa.
Attività valore
Le attività di valore sono struct usati per evitare allocazioni nella programmazione basata su attività. Un'attività valore è un valore temporaneo che viene trasformato in un'attività reale usando .AsTask()
.
Per creare un'attività di valore da un'espressione di attività, usare |> ValueTask<ReturnType>
o |> ValueTask
. Ad esempio:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
Aggiunta di token di annullamento e controlli di annullamento
A differenza delle espressioni asincrone F#, le espressioni di attività non passano in modo implicito un token di annullamento e non eseguono in modo implicito i controlli di annullamento. Se il codice richiede un token di annullamento, è necessario specificare il token di annullamento come parametro. Ad esempio:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
Se si intende rendere annullabile correttamente il codice, verificare attentamente di passare il token di annullamento a tutte le operazioni della libreria .NET che supportano l'annullamento. Ad esempio, Stream.ReadAsync
ha più overload, uno dei quali accetta un token di annullamento. Se non si usa questo overload, tale operazione di lettura asincrona specifica non sarà annullabile.
Attività in background
Per impostazione predefinita, le attività .NET vengono pianificate usando SynchronizationContext.Current se presenti. In questo modo, le attività possono fungere da agenti cooperativi e interleaved in esecuzione su un thread dell'interfaccia utente senza bloccare l'interfaccia utente. Se non è presente, le continuazioni delle attività vengono pianificate nel pool di thread .NET.
In pratica, è spesso consigliabile che il codice della libreria che genera attività ignori il contesto di sincronizzazione e passa sempre al pool di thread .NET, se necessario. A tale scopo, è possibile usare backgroundTask { }
:
backgroundTask { expression }
Un'attività in background ignora uno SynchronizationContext.Current
qualsiasi nel senso seguente: se viene avviato in un thread con non Null SynchronizationContext.Current
, passa a un thread in background nel pool di thread usando Task.Run
. Se viene avviato in un thread con null SynchronizationContext.Current
, viene eseguito nello stesso thread.
Nota
In pratica, ciò significa che le chiamate a ConfigureAwait(false)
non sono in genere necessarie nel codice dell'attività F#. Al contrario, le attività che devono essere eseguite in background devono essere create usando backgroundTask { ... }
. Qualsiasi associazione di attività esterna a un'attività in background verrà risincronizzata al SynchronizationContext.Current
completamento dell'attività in background.
Limitazioni delle attività relative alle chiamate di coda
A differenza delle espressioni asincrone F#, le espressioni di attività non supportano le codecall. Ovvero, quando return!
viene eseguita, l'attività corrente viene registrata come in attesa dell'attività il cui risultato viene restituito. Ciò significa che le funzioni ricorsive e i metodi implementati tramite espressioni di attività possono creare catene non associate di attività e possono usare stack o heap non associati. Si consideri il codice di esempio seguente:
let rec taskLoopBad (count: int) : Task<string> =
task {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! taskLoopBad (count-1)
}
let t = taskLoopBad 10000000
t.Wait()
Questo stile di codifica non deve essere usato con le espressioni di attività. Verrà creata una catena di 100000000 attività e verrà generato un oggetto StackOverflowException
. Se viene aggiunta un'operazione asincrona in ogni chiamata di ciclo, il codice userà un heap essenzialmente non associato. Prendere in considerazione il passaggio di questo codice per usare un ciclo esplicito, ad esempio:
let taskLoopGood (count: int) : Task<string> =
task {
for i in count .. 1 do
printfn $"looping... count = {count}"
return "done!"
}
let t = taskLoopGood 10000000
t.Wait()
Se sono necessarie chiamate di coda asincrone, usare un'espressione asincrona F#, che supporta le codecall. Ad esempio:
let rec asyncLoopGood (count: int) =
async {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! asyncLoopGood (count-1)
}
let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()
Implementazione dell'attività
Le attività vengono implementate usando codice ripristinabile, una nuova funzionalità in F# 6. Le attività vengono compilate in "Resumable State Machines" dal compilatore F#. Queste informazioni sono descritte in dettaglio nel codice RFC ripristinabile e in una sessione della community del compilatore F#.