Condividi tramite


Espressioni di compito

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 .

Collegamento tramite il 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. Dopo il ritorno 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 del flusso di controllo for .. in .. do, while .. do, try .. with .., try .. finally .., if .. then .. else, e if .. then ... Questi possono a loro volta includere ulteriori costrutti di attività, ad eccezione dei gestori with 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 e use! collegamenti

All'interno delle espressioni di attività, le associazioni use possono essere associate 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.

open System
open System.IO
open System.Security.Cryptography
task {
    // use IDisposable
    use httpClient = new Net.Http.HttpClient()
    // use! Task<IDisposable>
    use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
   
    // use IDisposable
    use aes = Aes.Create()
    aes.KeySize <- 256
    aes.GenerateIV()
    aes.GenerateKey()
    // do! Task
    do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")

    // use IAsyncDisposable
    use outputStream = File.Create "secret.enc"
    // use IDisposable
    use encryptor = aes.CreateEncryptor()
    // use IAsyncDisposable
    use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
    // do! Task
    do! exampleDomain.Content.CopyToAsync cryptoStream
}

Attività di valore

Le attività di valore sono struct usati per evitare allocazioni nella programmazione basata su attività. Un'attività di valore è un valore temporaneo che viene trasformato in una vera attività usando .AsTask().

Per creare un'attività di valore da un'espressione di attività, usare |> ValueTask<ReturnType> o |> ValueTask. Per esempio:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

and! associazioni (a partire da F# 10)

All'interno delle espressioni di attività, è possibile attendere simultaneamente più operazioni asincrone (Task<'T>, ValueTask<'T>e Async<'T> così via). Confrontare:

// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
    task {
        let! x = getX()
        let! y = getY()
        return x, y
    }

// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
    task {
        let! x = getX()
        and! y = getY()
        return x, y
    }

Aggiunta di token di annullamento e controlli di annullamento

A differenza delle espressioni asincrone F#, le espressioni task non passano implicitamente un token di annullamento e non effettuano implicitamente controlli di annullamento. Se il codice richiede un token di annullamento, è necessario specificare il token di annullamento come parametro. Per 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ù sovraccarichi, uno dei quali accetta un token di annullamento. Se non si utilizza questo overload, l'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 svolgere il ruolo di agenti cooperativi e intercalati in esecuzione su un thread dell'interfaccia utente senza bloccarla. 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 qualsiasi SynchronizationContext.Current nel seguente senso: se viene avviata su un thread con SynchronizationContext.Current non nullo, passa a un thread del pool in background usando Task.Run. Se viene avviato in un thread con null SynchronizationContext.Current, viene eseguito nello stesso thread.

Annotazioni

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 collegamento esterno a un'attività in background verrà risincronizzato con il SynchronizationContext.Current al completamento dell'attività in background.

Limitazioni delle attività relative alle chiamate di ritorno

A differenza delle espressioni asincrone F#, le espressioni di attività non supportano le chiamate di coda. 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 ad esempio il codice 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 si aggiunge un'operazione asincrona a ogni iterazione del ciclo, il codice utilizzerà un heap essenzialmente illimitato. Prendere in considerazione la modifica 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. Per 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#.

Vedere anche