Événement
Créer des applications intelligentes
17 mars, 21 h - 21 mars, 10 h
Rejoignez la série de rencontres pour créer des solutions IA évolutives basées sur des cas d’usage réels avec d’autres développeurs et experts.
S’inscrire maintenantCe navigateur n’est plus pris en charge.
Effectuez une mise à niveau vers Microsoft Edge pour tirer parti des dernières fonctionnalités, des mises à jour de sécurité et du support technique.
La programmation asynchrone est un mécanisme essentiel aux applications modernes pour diverses raisons. Il existe deux cas d’usage principaux que la plupart des développeurs rencontreront :
Bien que le travail en arrière-plan implique souvent l’utilisation de plusieurs threads, il est important de prendre en compte les concepts d’asynchronie et de multithreading séparément. En fait, ce sont des préoccupations distinctes et l’un n’implique pas l’autre. Cet article décrit plus en détail les concepts distincts.
Le point précédent, à savoir que l'asynchronie est indépendante de l'utilisation de plusieurs threads, mérite d'être expliqué un peu plus en détail. Il existe trois concepts qui sont parfois liés, mais strictement indépendants les uns des autres :
Les trois sont des concepts orthogonaux, mais ils peuvent être facilement confondus, en particulier lorsqu’ils sont utilisés ensemble. Par exemple, vous devrez peut-être exécuter plusieurs calculs asynchrones en parallèle. Cette relation ne signifie pas que le parallélisme ou l’asynchronie s'impliquent mutuellement.
Si vous considérez l’étymologie du mot asynchrone », il y a deux éléments en jeu :
Lorsque vous mettez ces deux termes ensemble, vous verrez que « asynchrone » signifie « pas en même temps ». Et voilà ! Cette définition n’implique pas la concurrence ou le parallélisme. Cela est également vrai dans la pratique.
En pratique, les calculs asynchrones en F# sont planifiés pour s’exécuter indépendamment du flux de programme principal. Cette exécution indépendante n’implique pas la concurrence ou le parallélisme, ni qu’un calcul se déroule toujours en arrière-plan. En fait, les calculs asynchrones peuvent même s’exécuter de manière synchrone, en fonction de la nature du calcul et de l’environnement dans lequel le calcul s’exécute.
Le principal enseignement que vous devez tirer est que les calculs asynchrones sont indépendants du flux principal du programme. Bien qu’il existe peu de garanties quant au moment ou à la façon dont un calcul asynchrone s’exécute, il existe certaines approches pour orchestrer et planifier ces opérations. Le reste de cet article explore les concepts fondamentaux de l’asynchronie F# et explique comment utiliser les types, fonctions et expressions intégrés à F#.
En F#, la programmation asynchrone est centrée autour de deux concepts fondamentaux : les calculs asynchrones et les tâches.
Async<'T>
avec les async { }
expressions, qui représente un calcul asynchrone composable qui peut être démarré pour former une tâche.Task<'T>
, avec l’task { }
expression, qui représente une tâche .NET en cours d’exécution.En règle générale, il est préférable d’utiliser task {…}
plutôt que async {…}
dans le nouveau code si vous interagissez avec des bibliothèques .NET qui utilisent des tâches, et si vous ne comptez pas sur des « tailcalls » de code asynchrones ou sur la propagation implicite de jetons d’annulation.
Vous pouvez voir les concepts de base de la programmation « asynchrone » dans l’exemple suivant :
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
Dans l’exemple, la fonction printTotalFileBytesUsingAsync
est de type string -> Async<unit>
. L’appel de la fonction n’exécute pas réellement le calcul asynchrone. Au lieu de cela, elle retourne un Async<unit>
qui agit comme une spécification du travail qui doit s’exécuter de manière asynchrone. Elle appelle Async.AwaitTask
dans son corps, ce qui convertit le résultat d’un ReadAllBytesAsync en type approprié.
Une autre ligne importante est l’appel à Async.RunSynchronously
. Il s’agit de l’une des fonctions de démarrage du module Async que vous devez appeler si vous souhaitez exécuter un calcul asynchrone F#.
Il s’agit d’une différence fondamentale avec le style C#/Visual Basic de la programmation async
. En F#, les calculs asynchrones peuvent être considérés comme des tâches à froid. Ils doivent être explicitement démarrés pour s’exécuter réellement. Cela présente quelques avantages, car cela vous permet de combiner et de séquencer le travail asynchrone beaucoup plus facilement qu’en C# ou Visual Basic.
Voici un exemple qui s’appuie sur le précédent en combinant des calculs :
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
Comme vous pouvez le voir, la fonction main
présente quelques éléments supplémentaires. Conceptuellement, elle effectue les opérations suivantes :
Async<unit>
avec Seq.map
.Async<'T[]>
qui planifie et exécute les calculs printTotalFileBytes
en parallèle lorsqu’il s’exécute.Async<unit>
qui exécute le calcul parallèle et ignore son résultat (qui est un unit[]
).Async.RunSynchronously
, qui bloque jusqu’à ce qu’il se termine.Lorsque ce programme s’exécute, printTotalFileBytes
s’exécute en parallèle pour chaque argument de ligne de commande. Étant donné que les calculs asynchrones s’exécutent indépendamment du flux de programme, il n’existe aucun ordre défini dans lequel ils impriment leurs informations et terminent de s’exécuter. Les calculs seront planifiés en parallèle, mais leur ordre d’exécution n’est pas garanti.
Étant donné que Async<'T>
est une spécification de travail plutôt que d’une tâche déjà en cours d’exécution, vous pouvez effectuer facilement des transformations plus complexes. Voici un exemple qui séquence un ensemble de calculs asynchrones afin qu’ils s’exécutent l’un après l’autre.
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
Cela planifie l’exécution par printTotalFileBytes
dans l’ordre des éléments de argv
plutôt que de les planifier en parallèle. Étant donné que chaque opération successive ne sera planifiée qu’après l’exécution du calcul précédent, les calculs sont séquencés afin qu’il n’y ait pas de chevauchement dans leur exécution.
Lorsque vous écrivez du code asynchrone en F#, vous interagirez généralement avec une infrastructure qui gère la planification des calculs pour vous. Toutefois, ce n’est pas toujours le cas, il est donc judicieux de comprendre les différentes fonctions qui peuvent être utilisées pour planifier le travail asynchrone.
Étant donné que les calculs asynchrones F# sont une spécification de travail plutôt qu’une représentation du travail déjà en cours d’exécution, ils doivent être explicitement démarrés avec une fonction de départ. Il existe de nombreuses méthodes de démarrage asynchrones qui sont utiles dans différents contextes. La section suivante décrit certaines des fonctions de démarrage les plus courantes.
Démarre un calcul enfant dans un calcul asynchrone. Cela permet à plusieurs calculs asynchrones d’être exécutés simultanément. Le calcul enfant partage un jeton d’annulation avec le calcul parent. Si le calcul parent est annulé, le calcul enfant est également annulé.
Signature :
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
Quand l’utiliser :
À quoi faut-il faire attention ?
Async.StartChild
n’est pas le même que la planification en parallèle. Si vous souhaitez planifier des calculs en parallèle, utilisez Async.Parallel
.Exécute un calcul asynchrone, en commençant immédiatement sur le thread du système d’exploitation actuel. Ceci est utile si vous devez mettre à jour quelque chose sur le thread appelant pendant le calcul. Par exemple, si un calcul asynchrone doit mettre à jour une interface utilisateur (par exemple, mettre à jour une barre de progression), Async.StartImmediate
doit être utilisé.
Signature :
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Quand l’utiliser :
À quoi faut-il faire attention ?
Async.StartImmediate
.Exécute un calcul dans le pool de threads. Retourne un Task<TResult> qui sera terminé à l’état correspondant une fois le calcul terminé (produit le résultat, génère une exception ou est annulé). Si aucun jeton d’annulation n’est fourni, le jeton d’annulation par défaut est utilisé.
Signature :
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
Quand l’utiliser :
À quoi faut-il faire attention ?
Task
supplémentaire, ce qui peut augmenter la surcharge si elle est souvent utilisée.Planifie une séquence de calculs asynchrones à exécuter en parallèle, ce qui génère un tableau de résultats dans l’ordre dans lequel ils ont été fournis. Le degré de parallélisme peut être éventuellement paramétré/limité en spécifiant le paramètre maxDegreeOfParallelism
.
Signature :
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Quand l’utiliser :
À quoi faut-il faire attention ?
Planifie une séquence de calculs asynchrones à exécuter dans l’ordre dans lequel ils sont passés. Le premier calcul sera exécuté, puis le suivant, et ainsi de suite. Aucun calcul n’est exécuté en parallèle.
Signature :
computations: seq<Async<'T>> -> Async<'T[]>
Quand l’utiliser :
À quoi faut-il faire attention ?
Retourne un calcul asynchrone qui attend la fin de l’opération Task<TResult> donnée et retourne son résultat sous la forme d’un Async<'T>
Signature :
task: Task<'T> -> Async<'T>
Quand l’utiliser :
À quoi faut-il faire attention ?
Crée un calcul asynchrone qui exécute un Async<'T>
donné, retournant un Async<Choice<'T, exn>>
. Si le Async<'T>
donné se termine correctement, un Choice1Of2
est retourné avec la valeur résultante. Si une exception est levée avant sa fin, un Choice2of2
est retourné avec l’exception levée. S’il est utilisé sur un calcul asynchrone qui est lui-même composé de nombreux calculs, et l’un de ces calculs génère une exception, le calcul englobant est entièrement arrêté.
Signature :
computation: Async<'T> -> Async<Choice<'T, exn>>
Quand l’utiliser :
À quoi faut-il faire attention ?
Crée un calcul asynchrone qui exécute le calcul donné, mais supprime son résultat.
Signature :
computation: Async<'T> -> Async<unit>
Quand l’utiliser :
ignore
pour le code non asynchrone.À quoi faut-il faire attention ?
Async.Ignore
parce que vous souhaitez utiliser Async.Start
ou une autre fonction qui nécessite Async<unit>
, envisagez si ignorer le résultat est acceptable. Évitez d’ignorer les résultats simplement pour s’adapter à une signature de type.Exécute un calcul asynchrone et attend son résultat sur le thread appelant. Propage une exception si le calcul en génère une. Cet appel est bloquant.
Signature :
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Quand l’utiliser :
À quoi faut-il faire attention ?
Async.RunSynchronously
bloque le thread appelant jusqu’à la fin de l’exécution.Démarre un calcul asynchrone qui retourne unit
dans le pool de threads. N’attend pas son achèvement et/ou observe un résultat d’exception. Les calculs imbriqués démarrés avec Async.Start
sont démarrés indépendamment du calcul parent qui les a appelés ; leur durée de vie n’est liée à aucun calcul parent. Si le calcul parent est annulé, aucun calcul enfant n’est annulé.
Signature :
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Utilisez uniquement quand :
À quoi faut-il faire attention ?
Async.Start
ne sont pas propagées à l’appelant. La pile des appels sera complètement déroulée.printfn
) démarré avec Async.Start
ne provoque pas d’effet sur le thread principal de l’exécution d’un programme.Si vous utilisez la programmation async { }
, vous devrez peut-être interagir avec une bibliothèque .NET ou une base de code C# qui utilise une programmation asynchrone de style async/await. Étant donné que C# et la majorité des bibliothèques .NET utilisent les types Task<TResult> et Task comme abstractions principales, cela peut changer la façon dont vous écrivez votre code asynchrone F#.
L’une des options consiste à passer à l’écriture de tâches .NET directement à l’aide de task { }
. Vous pouvez également utiliser la fonction Async.AwaitTask
pour attendre un calcul asynchrone .NET :
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Vous pouvez utiliser la fonction Async.StartAsTask
pour passer un calcul asynchrone à un appelant .NET :
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
Pour utiliser des API qui utilisent Task (autrement dit, les calculs asynchrones .NET qui ne retournent pas de valeur), vous devrez peut-être ajouter une fonction supplémentaire qui convertira un Async<'T>
en un Task :
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
Il existe déjà un Async.AwaitTask
qui accepte un Task en tant qu’entrée. Avec cette fonction et la fonction startTaskFromAsyncUnit
précédemment définie, vous pouvez démarrer et attendre des types Task à partir d’un calcul asynchrone F#.
En F#, vous pouvez écrire des tâches directement à l’aide de task { }
, par exemple :
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
Dans l’exemple, la fonction printTotalFileBytesUsingTasks
est de type string -> Task<unit>
. L’appel de la fonction commence à exécuter la tâche.
L’appel à task.Wait()
attend la fin de la tâche.
Bien que le threading soit mentionné tout au long de cet article, il y a deux choses importantes à retenir :
Par exemple, un calcul peut réellement s’exécuter sur le thread de son appelant, en fonction de la nature du travail. Un calcul peut également « sauter » entre les threads, en les empruntant pendant un petit temps pour effectuer des tâches utiles entre les périodes d'« attente » (par exemple, lorsqu’un appel réseau est en transit).
Bien que F# offre certaines capacités de démarrage d’un calcul asynchrone sur le thread actuel (ou explicitement pas sur le thread actuel), l’asynchronie n’est généralement pas associée à une stratégie de threading particulière.
Commentaires sur .NET
.NET est un projet open source. Sélectionnez un lien pour fournir des commentaires :
Événement
Créer des applications intelligentes
17 mars, 21 h - 21 mars, 10 h
Rejoignez la série de rencontres pour créer des solutions IA évolutives basées sur des cas d’usage réels avec d’autres développeurs et experts.
S’inscrire maintenantFormation
Parcours d’apprentissage
Premiers pas avec F# - Training
F# est un langage de programmation open source multiplateforme qui facilite l’écriture de code concis, performant, robuste et pratique. Ce langage à usage général vous permet de créer de nombreux types d’applications comme des applications d’API web, de bureau, IoT, de jeu et plus encore.