Eventi
17 mar, 21 - 21 mar, 10
Partecipa alla serie meetup per creare soluzioni di intelligenza artificiale scalabili basate su casi d'uso reali con altri sviluppatori ed esperti.
Iscriviti subitoQuesto browser non è più supportato.
Esegui l'aggiornamento a Microsoft Edge per sfruttare i vantaggi di funzionalità più recenti, aggiornamenti della sicurezza e supporto tecnico.
La programmazione asincrona è un meccanismo essenziale per le applicazioni moderne per diversi motivi. Esistono due casi d'uso principali che la maggior parte degli sviluppatori incontrerà:
Anche se il lavoro in background comporta spesso l'utilizzo di più thread, è importante considerare separatamente i concetti di asincronia e multithreading. In effetti, sono preoccupazioni separate, e uno non implica l'altro. Questo articolo descrive in modo più dettagliato i concetti separati.
Il punto precedente, che l'asincronia è indipendente dall'utilizzo di più thread, vale la pena spiegare un po' più in dettaglio. Esistono tre concetti che a volte sono correlati, ma strettamente indipendenti l'uno dall'altro:
Tutti e tre sono concetti ortogonali, ma possono essere facilmente gonfiati, soprattutto quando vengono usati insieme. Ad esempio, potrebbe essere necessario eseguire più calcoli asincroni in parallelo. Questa relazione non significa che il parallelismo o l'asincronia implicano l'uno l'altro.
Se si considera l'accuratezza della parola "asincrona", ci sono due parti coinvolte:
Quando si inseriscono questi due termini insieme, si noterà che "asincrono" significa "non contemporaneamente". Ecco fatto! Non c'è alcuna implicazione della concorrenza o del parallelismo in questa definizione. Questo vale anche in pratica.
In termini pratici, i calcoli asincroni in F# vengono pianificati per l'esecuzione indipendentemente dal flusso del programma principale. Questa esecuzione indipendente non implica concorrenza o parallelismo, né implica che un calcolo avviene sempre in background. In effetti, i calcoli asincroni possono anche essere eseguiti in modo sincrono, a seconda della natura del calcolo e dell'ambiente in cui è in esecuzione il calcolo.
L'aspetto principale da tenere è che i calcoli asincroni sono indipendenti dal flusso principale del programma. Anche se esistono poche garanzie su quando o come viene eseguito un calcolo asincrono, esistono alcuni approcci per orchestrarli e pianificarli. Il resto di questo articolo illustra i concetti di base per F# asincronia e come usare i tipi, le funzioni e le espressioni incorporati in F#.
In F# la programmazione asincrona è centrata su due concetti di base: calcoli asincroni e attività.
Async<'T>
con async { }
espressioni, che rappresenta un calcolo asincrono componibile che può essere avviato per formare un'attività.Task<'T>
, con task { }
espressioni, che rappresenta un'attività .NET in esecuzione.In generale, è consigliabile usare task {…}
over async {…}
in un nuovo codice se si interagisce con le librerie .NET che usano attività e se non ci si basa su codecall asincrone o la propagazione implicita dei token di annullamento.
È possibile visualizzare i concetti di base della programmazione "asincrona" nell'esempio seguente:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
Nell'esempio la printTotalFileBytesUsingAsync
funzione è di tipo string -> Async<unit>
. La chiamata alla funzione non esegue effettivamente il calcolo asincrono. Restituisce invece un oggetto Async<unit>
che funge da specifica del lavoro da eseguire in modo asincrono. Async.AwaitTask
Chiama nel corpo, che converte il risultato di ReadAllBytesAsync in un tipo appropriato.
Un'altra riga importante è la chiamata a Async.RunSynchronously
. Si tratta di una delle funzioni iniziali del modulo asincrono che è necessario chiamare se si vuole effettivamente eseguire un calcolo asincrono F#.
Questa è una differenza fondamentale con lo stile di async
programmazione C#/Visual Basic. In F# i calcoli asincroni possono essere considerati come attività ad accesso sporadico. Devono essere avviati in modo esplicito per l'esecuzione effettiva. Questo presenta alcuni vantaggi, in quanto consente di combinare e sequenziare il lavoro asincrono molto più facilmente rispetto a C# o Visual Basic.
Di seguito è riportato un esempio basato su quello precedente combinando i calcoli:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
Come si può notare, la main
funzione ha alcuni altri elementi. Concettualmente, esegue le operazioni seguenti:
Async<unit>
calcoli con Seq.map
.Async<'T[]>
che pianifica ed esegue i calcoli in parallelo durante l'esecuzione printTotalFileBytes
.Async<unit>
che eseguirà il calcolo parallelo e ne ignorerà il risultato (ovvero un ).unit[]
Async.RunSynchronously
, bloccando fino al completamento.Quando questo programma viene eseguito, printTotalFileBytes
viene eseguito in parallelo per ogni argomento della riga di comando. Poiché i calcoli asincroni vengono eseguiti indipendentemente dal flusso del programma, non esiste un ordine definito in cui stampano le informazioni e terminano l'esecuzione. I calcoli verranno pianificati in parallelo, ma l'ordine di esecuzione non è garantito.
Poiché Async<'T>
è una specifica di lavoro anziché un'attività già in esecuzione, è possibile eseguire più facilmente trasformazioni più complesse. Di seguito è riportato un esempio che sequenzia un set di calcoli asincroni in modo che vengano eseguiti uno dopo l'altro.
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
In questo modo verrà pianificata printTotalFileBytes
l'esecuzione nell'ordine degli elementi di argv
anziché pianificarli in parallelo. Poiché ogni operazione successiva non verrà pianificata fino al termine dell'esecuzione del calcolo precedente, i calcoli vengono sequenziati in modo che non vi siano sovrapposizioni nell'esecuzione.
Quando si scrive codice asincrono in F#, in genere si interagisce con un framework che gestisce automaticamente la pianificazione dei calcoli. Tuttavia, questo non è sempre il caso, quindi è bene comprendere le varie funzioni che possono essere usate per pianificare il lavoro asincrono.
Poiché i calcoli asincroni F# sono una specifica di lavoro anziché una rappresentazione di lavoro già in esecuzione, devono essere avviati in modo esplicito con una funzione iniziale. Esistono molti metodi di avvio asincroni utili in contesti diversi. La sezione seguente descrive alcune delle funzioni di avvio più comuni.
Avvia un calcolo figlio all'interno di un calcolo asincrono. Ciò consente l'esecuzione simultanea di più calcoli asincroni. Il calcolo figlio condivide un token di annullamento con il calcolo padre. Se il calcolo padre viene annullato, viene annullato anche il calcolo figlio.
Firma:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Uso consigliato:
Cosa tenere d'occhio per:
Async.StartChild
non equivale alla pianificazione in parallelo. Se si desidera pianificare i calcoli in parallelo, usare Async.Parallel
.Esegue un calcolo asincrono, a partire immediatamente dal thread del sistema operativo corrente. Ciò è utile se è necessario aggiornare un elemento nel thread chiamante durante il calcolo. Ad esempio, se un calcolo asincrono deve aggiornare un'interfaccia utente , ad esempio l'aggiornamento di un indicatore di stato, Async.StartImmediate
deve essere usata.
Firma:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Uso consigliato:
Cosa tenere d'occhio per:
Async.StartImmediate
è probabilmente inappropriato da usare.Esegue un calcolo nel pool di thread. Restituisce un Task<TResult> oggetto che verrà completato sullo stato corrispondente al termine del calcolo (produce il risultato, genera un'eccezione o viene annullata). Se non viene fornito alcun token di annullamento, viene usato il token di annullamento predefinito.
Firma:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Uso consigliato:
Cosa tenere d'occhio per:
Task
, che può aumentare il sovraccarico se viene usato spesso.Pianifica una sequenza di calcoli asincroni da eseguire in parallelo, producendo una matrice di risultati nell'ordine in cui sono stati forniti. Il grado di parallelismo può essere facoltativamente ottimizzato/limitato specificando il maxDegreeOfParallelism
parametro .
Firma:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Quando usarlo:
Cosa tenere d'occhio per:
Pianifica una sequenza di calcoli asincroni da eseguire nell'ordine in cui vengono passati. Il primo calcolo verrà eseguito, quindi il successivo e così via. Non verranno eseguiti calcoli in parallelo.
Firma:
computations: seq<Async<'T>> -> Async<'T[]>
Quando usarlo:
Cosa tenere d'occhio per:
Restituisce un calcolo asincrono che attende il completamento dell'oggetto specificato Task<TResult> e ne restituisce il risultato come Async<'T>
Firma:
task: Task<'T> -> Async<'T>
Uso consigliato:
Cosa tenere d'occhio per:
Crea un calcolo asincrono che esegue un determinato Async<'T>
oggetto , restituendo un oggetto Async<Choice<'T, exn>>
. Se l'oggetto specificato Async<'T>
viene completato correttamente, viene restituito un Choice1Of2
oggetto con il valore risultante. Se viene generata un'eccezione prima del completamento, viene restituito un Choice2of2
oggetto con l'eccezione generata. Se viene usato in un calcolo asincrono composto da molti calcoli e uno di questi calcoli genera un'eccezione, il calcolo che include verrà arrestato completamente.
Firma:
computation: Async<'T> -> Async<Choice<'T, exn>>
Uso consigliato:
Cosa tenere d'occhio per:
Crea un calcolo asincrono che esegue il calcolo specificato, ma ne elimina il risultato.
Firma:
computation: Async<'T> -> Async<unit>
Uso consigliato:
ignore
codice non asincrono.Cosa tenere d'occhio per:
Async.Ignore
perché si vuole usare Async.Start
o un'altra funzione che richiede Async<unit>
, valutare se ignorare il risultato è corretto. Evitare di eliminare i risultati solo per adattare una firma di tipo.Esegue un calcolo asincrono e attende il risultato sul thread chiamante. Propaga un'eccezione se il calcolo ne restituisce uno. Questa chiamata sta bloccando.
Firma:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Quando usarlo:
Cosa tenere d'occhio per:
Async.RunSynchronously
blocca il thread chiamante fino al completamento dell'esecuzione.Avvia un calcolo asincrono che restituisce unit
nel pool di thread. Non attende il completamento e/o osserva un risultato di eccezione. I calcoli annidati avviati con Async.Start
vengono avviati indipendentemente dal calcolo padre che li ha chiamati; la loro durata non è associata ad alcun calcolo padre. Se il calcolo padre viene annullato, non vengono annullati calcoli figlio.
Firma:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Usare solo quando:
Cosa tenere d'occhio per:
Async.Start
non vengono propagate al chiamante. Lo stack di chiamate sarà completamente scollegato.printfn
) avviato con Async.Start
non causerà l'effetto sul thread principale dell'esecuzione di un programma.Se si usa async { }
la programmazione, potrebbe essere necessario interagire con una libreria .NET o una codebase C# che usa programmazione asincrona asincrona/await. Poiché C# e la maggior parte delle librerie .NET usano i Task<TResult> tipi e Task come astrazioni principali, questo può cambiare il modo in cui si scrive il codice asincrono F#.
Un'opzione consiste nel passare alla scrittura di attività .NET direttamente tramite task { }
. In alternativa, è possibile usare la Async.AwaitTask
funzione per attendere un calcolo asincrono .NET:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
È possibile usare la Async.StartAsTask
funzione per passare un calcolo asincrono a un chiamante .NET:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Per usare le API che usano Task (vale a dire calcoli asincroni .NET che non restituiscono un valore), potrebbe essere necessario aggiungere un'altra funzione che convertirà un oggetto Async<'T>
in :Task
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Esiste già un oggetto Async.AwaitTask
che accetta come Task input. Con questa e la funzione definita startTaskFromAsyncUnit
in precedenza, è possibile avviare e attendere Task i tipi da un calcolo asincrono F#.
In F# è possibile scrivere attività direttamente usando task { }
, ad esempio:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
Nell'esempio la printTotalFileBytesUsingTasks
funzione è di tipo string -> Task<unit>
. La chiamata alla funzione inizia a eseguire l'attività.
La chiamata a task.Wait()
attende il completamento dell'attività.
Anche se il threading è menzionato in questo articolo, esistono due aspetti importanti da ricordare:
Ad esempio, un calcolo può effettivamente essere eseguito sul thread del chiamante, a seconda della natura del lavoro. Un calcolo potrebbe anche "saltare" tra thread, prendendoli in prestito per una piccola quantità di tempo per svolgere un lavoro utile tra periodi di "attesa", ad esempio quando una chiamata di rete è in transito.
Anche se F# offre alcune capacità di avviare un calcolo asincrono sul thread corrente (o in modo esplicito non nel thread corrente), l'asincronia in genere non è associata a una particolare strategia di threading.
Feedback su .NET
.NET è un progetto di open source. Selezionare un collegamento per fornire feedback:
Eventi
17 mar, 21 - 21 mar, 10
Partecipa alla serie meetup per creare soluzioni di intelligenza artificiale scalabili basate su casi d'uso reali con altri sviluppatori ed esperti.
Iscriviti subitoFormazione
Percorso di apprendimento
Muovere i primi passi con F# - Training
F# è un linguaggio di programmazione open source multipiattaforma che semplifica la scrittura di codice conciso, con prestazioni elevate, affidabile e pratico. È un linguaggio per utilizzo generico che consente di creare molti tipi diversi di applicazioni, ad esempio API Web, desktop, IoT, giochi e altro ancora.
Documentazione
Informazioni sul supporto nel linguaggio di programmazione F# per la scrittura di espressioni di attività, che creano direttamente attività .NET.
Informazioni su come creare una sintassi pratica per la scrittura di calcoli in F# che possono essere sequenziati e combinati usando costrutti e associazioni del flusso di controllo.
Informazioni su come le espressioni lazy F# possono migliorare le prestazioni delle app e delle librerie.