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:

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 .. dodel flusso di controllo , , while .. dotry .. 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.

useassociazioni 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#.

Vedi anche