Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
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 :
- Présentation d’un processus serveur qui peut traiter un nombre important de requêtes entrantes simultanées, tout en réduisant les ressources système occupées pendant que le traitement des demandes attend les entrées des systèmes ou services externes à ce processus
- Gestion d’une interface utilisateur réactive ou d’un thread principal lors de la progression simultanée du travail en arrière-plan
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, ils sont des préoccupations distinctes et l’un n’implique pas l’autre. Cet article décrit plus en détail les concepts distincts.
Asynchrony défini
Le point précédent - que l’asynchronie est indépendante de l’utilisation de plusieurs threads - vaut la peine d’expliquer un peu plus loin. Il existe trois concepts qui sont parfois liés, mais strictement indépendants les uns des autres :
- Concurrence; lorsque plusieurs calculs s’exécutent dans des périodes qui se chevauchent.
- Parallélisme; lorsque plusieurs calculs ou plusieurs parties d’un seul calcul s’exécutent exactement en même temps.
- Asynchronie ; lorsqu’un ou plusieurs calculs peuvent s’exécuter séparément du flux de programme principal.
Les trois sont des concepts orthogonaux, mais peuvent être facilement conflatés, 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 impliquent l’un de l’autre.
Si vous considérez l’étymologie du mot « asynchrone », deux éléments sont impliqués :
- « a », ce qui signifie « non ».
- « synchrone », ce qui signifie « en même temps ».
Lorsque vous mettez ces deux termes ensemble, vous verrez que « asynchrone » signifie « pas en même temps ». C’est tout ! Il n’y a aucune implication de l’accès concurrentiel ou du parallélisme dans cette définition. 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 l’accès concurrentiel ou le parallélisme, ni n’implique-t-elle qu’un calcul se produit 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.
La principale chose à prendre, c’est que les calculs asynchrones sont indépendants du flux de programme principal. 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#.
Concepts de base
En F#, la programmation asynchrone est centrée autour de deux concepts fondamentaux : les calculs asynchrones et les tâches.
- Type
Async<'T>
avecasync { }
des expressions, qui représente un calcul asynchrone composable qui peut être démarré pour former une tâche. - Type
Task<'T>
, avectask { }
des expressions, qui représente une tâche .NET en cours d’exécution.
En général, vous devez envisager de l’utiliser task {…}
async {…}
dans le nouveau code si vous interagissez avec les bibliothèques .NET qui utilisent des tâches, et si vous ne vous appuyez pas sur les appels de code asynchrones ou la propagation implicite des jetons d’annulation.
Concepts fondamentaux d’async
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 printTotalFileBytesUsingAsync
fonction est de type string -> Async<unit>
. L’appel de la fonction n’exécute pas réellement le calcul asynchrone. Au lieu de cela, il retourne un Async<unit>
qui agit comme une spécification du travail qui doit s’exécuter de manière asynchrone. Il appelle Async.AwaitTask
dans son corps, ce qui convertit le résultat d’un ReadAllBytesAsync 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 async
la programmation. Dans 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 il vous permet de combiner et de séquencer le travail asynchrone beaucoup plus facilement que dans C# ou Visual Basic.
Combiner des calculs asynchrones
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 main
fonction a quelques éléments supplémentaires. Conceptuellement, il effectue les opérations suivantes :
- Transformez les arguments de ligne de commande en une séquence de
Async<unit>
calculs avecSeq.map
. - Créez une
Async<'T[]>
planification et exécute lesprintTotalFileBytes
calculs en parallèle lorsqu’il s’exécute. - Créez un
Async<unit>
qui exécutera le calcul parallèle et ignorez son résultat (qui est ununit[]
). - Exécutez explicitement le calcul composé global avec
Async.RunSynchronously
, bloquant 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 l’exécution. Les calculs seront planifiés en parallèle, mais leur ordre d’exécution n’est pas garanti.
Calculs asynchrones de séquence
Étant donné qu’il Async<'T>
s’agit d’une spécification de travail plutôt que d’une tâche déjà en cours d’exécution, vous pouvez effectuer des transformations plus complexes facilement. 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 printTotalFileBytes
l’exécution dans l’ordre des éléments plutôt argv
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.
Fonctions importantes du module Async
Lorsque vous écrivez du code asynchrone en F#, vous interagissez 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.
Async.StartChild
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 utiliser :
- Lorsque vous souhaitez exécuter plusieurs calculs asynchrones simultanément plutôt qu’un à la fois, mais ne les avez pas planifiés en parallèle.
- Lorsque vous souhaitez lier la durée de vie d’un calcul enfant à celui d’un calcul parent.
Qu’est-ce à regarder pour :
- Le démarrage de plusieurs calculs
Async.StartChild
avec n’est pas le même que la planification en parallèle. Si vous souhaitez planifier des calculs en parallèle, utilisezAsync.Parallel
. - L’annulation d’un calcul parent déclenche l’annulation de tous les calculs enfants qu’il a démarrés.
Async.StartImmediate
Exécute un calcul asynchrone, en commençant immédiatement sur le thread du système d’exploitation actuel. Cela 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), il Async.StartImmediate
doit être utilisé.
Signature:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
Quand utiliser :
- Lorsque vous devez mettre à jour quelque chose sur le thread appelant au milieu d’un calcul asynchrone.
Qu’est-ce à regarder pour :
- Le code dans le calcul asynchrone s’exécute sur le thread sur lequel un thread doit être planifié. Cela peut être problématique si ce thread est sensible d’une certaine manière, tel qu’un thread d’interface utilisateur. Dans ce cas,
Async.StartImmediate
il est probable qu’il n’est pas approprié d’utiliser.
Async.StartAsTask
Exécute un calcul dans le pool de threads. Retourne un Task<TResult> résultat qui sera terminé à l’état correspondant une fois le calcul terminé (produit le résultat, lève 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 utiliser :
- Lorsque vous devez appeler une API .NET qui génère un Task<TResult> résultat de calcul asynchrone.
Qu’est-ce à regarder pour :
- Cet appel alloue un objet supplémentaire
Task
, ce qui peut augmenter la surcharge si elle est souvent utilisée.
Async.Parallel
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 maxDegreeOfParallelism
paramètre.
Signature:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
Quand l’utiliser :
- Si vous avez besoin d’exécuter un ensemble de calculs en même temps et que vous n’avez aucune dépendance sur leur ordre d’exécution.
- Si vous n’avez pas besoin de résultats de calculs planifiés en parallèle tant qu’ils n’ont pas tous été terminés.
Qu’est-ce à regarder pour :
- Vous ne pouvez accéder au tableau de valeurs résultant qu’une fois tous les calculs terminés.
- Les calculs sont exécutés chaque fois qu’ils finissent par être planifiés. Ce comportement signifie que vous ne pouvez pas compter sur leur ordre d’exécution.
Async.Sequential
Planifie une séquence de calculs asynchrones à exécuter dans l’ordre dans lequel elles sont passées. 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 :
- Si vous devez exécuter plusieurs calculs dans l’ordre.
Qu’est-ce à regarder pour :
- Vous ne pouvez accéder au tableau de valeurs résultant qu’une fois tous les calculs terminés.
- Les calculs sont exécutés dans l’ordre dans lequel ils sont passés à cette fonction, ce qui peut signifier qu’un plus grand temps s’écoule avant que les résultats ne soient retournés.
Async.AwaitTask
Retourne un calcul asynchrone qui attend la fin de l’opération donnée Task<TResult> et retourne son résultat sous la forme d’un Async<'T>
Signature:
task: Task<'T> -> Async<'T>
Quand utiliser :
- Lorsque vous consommez une API .NET qui retourne un Task<TResult> calcul asynchrone F# .
Qu’est-ce à regarder pour :
- Les exceptions sont encapsulées dans AggregateException la convention de la bibliothèque parallèle de tâches. Ce comportement diffère de la façon dont F# async expose généralement les exceptions.
Async.Catch
Crée un calcul asynchrone qui exécute un calcul donné Async<'T>
, en retournant un Async<Choice<'T, exn>>
. Si l’opération donnée Async<'T>
se termine correctement, une Choice1Of2
valeur est retournée avec la valeur résultante. Si une exception est levée avant sa fin, une Choice2of2
exception est retournée 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 lève une exception, le calcul englobant est entièrement arrêté.
Signature:
computation: Async<'T> -> Async<Choice<'T, exn>>
Quand utiliser :
- Lorsque vous effectuez un travail asynchrone qui peut échouer avec une exception et que vous souhaitez gérer cette exception dans l’appelant.
Qu’est-ce à regarder pour :
- Lorsque vous utilisez des calculs asynchrones combinés ou séquencés, le calcul englobant s’arrête entièrement si l’un de ses calculs « internes » lève une exception.
Async.Ignore
Crée un calcul asynchrone qui exécute le calcul donné, mais supprime son résultat.
Signature:
computation: Async<'T> -> Async<unit>
Quand utiliser :
- Lorsque vous disposez d’un calcul asynchrone dont le résultat n’est pas nécessaire. Cela est analogue à la fonction pour le
ignore
code non asynchrone.
Qu’est-ce à regarder pour :
- Si vous devez utiliser
Async.Ignore
parce que vous souhaitez utiliserAsync.Start
ou une autre fonction qui nécessiteAsync<unit>
, envisagez si le résultat est correct. Évitez d’ignorer les résultats simplement pour s’adapter à une signature de type.
Async.RunSynchronously
Exécute un calcul asynchrone et attend son résultat sur le thread appelant. Propage une exception si le calcul en génère un. Cet appel bloque.
Signature:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
Quand l’utiliser :
- Si vous en avez besoin, utilisez-la une seule fois dans une application , au point d’entrée d’un exécutable.
- Lorsque vous ne vous souciez pas des performances et que vous souhaitez exécuter un ensemble d’autres opérations asynchrones à la fois.
Qu’est-ce à regarder pour :
- L’appel
Async.RunSynchronously
bloque le thread appelant jusqu’à la fin de l’exécution.
Async.Start
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 sont Async.Start
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 :
- Vous disposez d’un calcul asynchrone qui ne génère pas de résultat et/ou nécessite un traitement d’un.
- Vous n’avez pas besoin de savoir quand un calcul asynchrone se termine.
- Vous ne vous souciez pas du thread sur lequel un calcul asynchrone s’exécute.
- Vous n’avez pas besoin de connaître ou de signaler des exceptions résultant de l’exécution.
Qu’est-ce à regarder pour :
- Les exceptions déclenchées par les calculs démarrés
Async.Start
ne sont pas propagées à l’appelant. La pile des appels sera complètement déwound. - Tout travail (tel que l’appel
printfn
) démarréAsync.Start
ne provoque pas l’effet sur le thread principal de l’exécution d’un programme.
Interopérabilité avec .NET
Si vous utilisez async { }
la programmation, vous devrez peut-être interagir avec une bibliothèque .NET ou une base de code C# qui utilise une programmation asynchrone de style asynchrone/await. Étant donné que C# et la majorité des bibliothèques .NET utilisent les Task<TResult> types et Task les types 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 task { }
de . Vous pouvez également utiliser la Async.AwaitTask
fonction pour attendre un calcul asynchrone .NET :
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
Vous pouvez utiliser la Async.StartAsTask
fonction 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à une Async.AwaitTask
entrée qui accepte une Task entrée. Avec cette fonction et la fonction précédemment définie startTaskFromAsyncUnit
, vous pouvez démarrer et attendre Task des types à partir d’un calcul asynchrone F#.
Écriture de tâches .NET directement dans F#
En F#, vous pouvez écrire des tâches directement à l’aide task { }
de :
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 printTotalFileBytesUsingTasks
fonction est de type string -> Task<unit>
. L’appel de la fonction commence à exécuter la tâche.
L’appel à task.Wait()
attendre la fin de la tâche.
Relation avec le multithreading
Bien que le threading soit mentionné dans cet article, il existe deux points importants à mémoriser :
- Il n’existe aucune affinité entre un calcul asynchrone et un thread, sauf si elle a démarré explicitement sur le thread actuel.
- La programmation asynchrone en F# n’est pas une abstraction pour le multithreading.
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.