Parallélisme, concurrence
Table des matières
1. Introduction

OCaml permet différentes sortes de parallélisme/concurrence:

  • par l'utilisation de threads (ou processus légers),
  • via des processus lourds et de la communication inter-processus,
  • ou en utilisant des bibliothèques comme Async ou Lwt permettant de ne pas bloquer le programme sur des opérations potentiellement bloquantes (lectures/écritures de fichiers, entrées/sorties réseau, attente d'événements d'interfaces graphiques, ...).

Parallélisme et concurrence sont deux choses différentes1.

La concurrence est l'accès simultané par plusieurs processus à une même ressource. Il convient en général de protéger ces accès afin de s'assurer qu'un seul processus à la fois accède à la ressource, de façon à ce qu'il ne soit pas interrompu pendant l'opération qui concerne cette ressource. Un exercice classique est la gestion d'un compteur pour obtenir des identifiants uniques.

Un algorithme de type MapReduce utilise le parallélisme pour effectuer de nombreux calculs simultanément, mais sans concurrence entre eux à priori, chacun utilisant ses propres ressources. Les résultats de ces calculs sont ensuite agrégés.

Il est donc tout à fait possible d'avoir des calculs en parallèle sans concurrence, de même que les problèmes de concurrence peuvent être traités avec un pseudo-parallélisme, comme c'est le cas avec les bibliothèques Lwt et Async.

Un2 problème d'OCaml vis-à-vis du parallélisme et de la concurrence est que le glaneur de cellules (garbage collector, ou GC) n'est pas concurrent et que, lorsqu'il entre en action, il bloque tout le processus en cours, y compris les éventuels threads (processus légers). Des travaux de recherche sont en cours pour fournir un GC concurrent à OCaml.

Nous allons maintenant explorer les différentes solutions3 pour effectuer des calculs en parallèle et/ou en concurrence, selon les trois approches listées plus haut. Chaque fois, nous listerons les bibliothèques utilisables mais nous nous contenterons d'exemples sur seulement certaines d'entre elles.

2. Monades et (non-)concurrence

La bibliothèque Lwt (pour Lightweight threads, ou processus légers légers) permet de simuler l'utilisation de threads en profitant du fait que certaines opérations sont bloquantes (les entrées/sorties ou l'attente d'autres événements) et qu'en attendant il est possible d'exécuter d'autres parties du code.

Pour utiliser la bibliothèque Lwt dans Utop, on utilisera dans l'ordre les deux directives suivantes, la première pour charger le module topfind qui donne accès à de nouvelles directives, dont la seconde qui charge le paquet lwt.simple-top et toutes ses dépendances:

#use "topfind";;
#require "lwt.simple-top";;
2.1. Principes

La bibliothèque Lwt utilise une monade4 basée sur un type représentant les "threads" Lwt, Lwt.t, et les deux opérations Lwt.bind et Lwt.return.

Au lieu d'utiliser les fonctions d'entrée/sortie habituelles comme Pervasives.input_line, on utilisera les variantes fournies par Lwt, comme Lwt_io.read_line. Regardons leurs types respectifs:

# Pervasives.input_line;;
- : in_channel -> string = <fun>
# Lwt_io.read_line;;
- : Lwt_io.input_channel -> string Lwt.t = <fun>

Hormis le fait que Lwt_io définit un nouveau type pour les canaux de lecture, ce qui différencie les deux fonctions est le type de retour. Dans le cas de Lwt_io.read_line, c'est un thread Lwt qui est retourné. Ce thread Lwt sera en attente de pouvoir lire sur le canal indiqué, et la fonction Lwt_io.read_channel retournera immédiatement sans bloquer.

Pour utiliser la ligne lue par la fonction, on utilisera la fonction Lwt.bind, qui prend en paramètre un thread Lwt et une fonction. Cette fonction sera appelée quand le thread Lwt aura terminé, avec en paramètre la valeur retournée par le thread Lwt:

# Lwt.bind;;
- : 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t = <fun>

La fonction en paramètre de Lwt.bind retourne elle aussi un thread Lwt, ceci afin de pouvoir composer les appels de fonctions.

Pour créer un thread Lwt retournant une valeur, il suffit d'utiliser la deuxième opération de la monade, Lwt.return, qui prend une valeur et en fait un thread Lwt qui est déjà terminé:

# Lwt.return;;
- : 'a -> 'a Lwt.t = <fun>
# Lwt.return "hello";;
- : string Lwt.t = <abstr>

A ce point, on peut se rendre compte qu'il n'y a pas de réel parallélisme, puisque les threads Lwt ne sont pas créés pour exécuter une fonction en parallèle, comme le permettent les threads systèmes (avec Thread.create évoqué plus loin), mais pour empaqueter une valeur dans un type (le type de la monade).

Le "parallélisme" vient du fait que Lwt fournit des fonctions non bloquantes pour les entrées/sorties, et son moteur s'occupe de créer les threads Lwt représentant les attentes correspondant à ces entrées/sorties, et de signaler ces threads comme terminés lorsque l'attente est terminée5. Cela signifie que tout ce qui n'est pas en attente d'une entrée/sortie pourra s'exécuter, ce qui fournit une forme de parallélisme entre ce qui attend et ce qui n'a pas besoin d'attendre.

Il n'y a cependant pas de parallélisme entre les parties du code qui n'ont pas besoin d'attendre. Si une partie met potentiellement du temps à s'exécuter ou si, pour une raison ou une autre, elle fait appel à une fonction bloquante, il convient de la faire s'exécuter dans un thread léger séparé. Lwt offre pour cela la fonction Lwt_preemptive.detach.

Ce modèle évite bien des problèmes de concurrence que l'on rencontre lorsqu'on est en présence de codes s'exécutant effectivement en parallèle.

De plus, Lwt inclut des bibliothèques additionnelles pour faire par exemple de la programmation événementielle, permettant de créer des threads Lwt représentant l'attente d'un événement.

Enfin, aucun thread en attente ne sera débloqué si l'ordonnanceur (scheduler) n'est pas lancé, par la fonction Lwt_main.run, sur le "thread principal":

# Lwt_main.run;;
- : 'a Lwt.t -> 'a = <fun>
2.2. Syntaxe

Pour faciliter l'écriture de code utilisant Lwt, la bibliothèque fournit quelques opérateurs et une extension de syntaxe.

Plutôt que la fonction Lwt.bind, on pourra utiliser deux opérateurs infixes prenant les mêmes paramètres dans un sens ou dans l'autre:

# open Lwt.Infix;;
# (>>=);;
- : 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t = <fun>
# (=<<);;
- : ('a -> 'b Lwt.t) -> 'a Lwt.t -> 'b Lwt.t = <fun>

Ainsi, les deux codes suivants sont équivalents pour créer un thread Lwt qui, lorsqu'une ligne aura été saisie sur l'entrée standard, retournera cette ligne mise en majuscules:

# Lwt.bind (Lwt_io.read_line Lwt_io.stdin) (fun s -> Lwt.return (String.uppercase s));;
- : string Lwt.t = <abstr>
# Lwt_io.read_line Lwt_io.stdin >>= fun s -> Lwt.return (String.uppercase s);;
- : string Lwt.t = <abstr>

La fonction Lwt.map permet de ne pas avoir à utiliser Lwt.return, qu'elle utilise pour créer un thread Lwt à partir du résultat d'une fonction:

# Lwt.map;;
- : ('a -> 'b) -> 'a Lwt.t -> 'b Lwt.t = <fun>

Les opérateurs (>|=) et (<|=) sont équivalents à Lwt.map avec deux façons d'ordonner les paramètres. Ainsi, notre code précédent devient:

# Lwt.map String.uppercase (Lwt_io.read_line Lwt_io.stdin) ;;
- : string Lwt.t = <abstr>
# Lwt_io.read_line Lwt_io.stdin >|= String.uppercase;;
- : string Lwt.t = <abstr>
# String.uppercase =|< Lwt_io.read_line Lwt_io.stdin;;
- : string Lwt.t = <abstr>
OCaml ≥ 4.02.0

Lwt fournit également une extension ppx facilitant encore l'écriture de code. Pour l'activer, on passera l'option -ppx ppx_lwt à ocaml, ocamlc, ..., ou l'option -package lwt.ppx à ocamlfind.

La construction

let%lwt x = e1
and y = e2 in
code

peut être utilisée à la place de

let t = e1
and t2 = e2 in
Lwt.bind t (fun x -> Lwt.bind t2 (fun y -> code)

Ainsi les deux constructions suivantes sont équivalentes:

# let p =
  let t = Lwt_io.read_line Lwt_io.stdin in
  Lwt.bind t (fun s -> Lwt.return (String.uppercase s));;
val p : string Lwt.t = <abstr>
# let p =
  let%lwt s = Lwt_io.read_line Lwt_io.stdin in
  Lwt.return (String.uppercase s);;
val p : string Lwt.t = <abstr>

La notation %lwt peut être utilisée dans d'autres constructions (match%lwt ... with, try%lwt ... with, ...); pour plus de détails, on consultera la documentation.

OCaml ≥ 4.02.0
2.3. Manipulation des threads

Les threads Lwt sont des valeurs OCaml comme les autres, et peuvent donc être passées à des fonctions. Lwt offre la possibilité d'effectuer plusieurs opérations sur ses threads. On peut par exemple demander l'état d'un thread, qui peut être endormi (en attente, Sleep), terminé avec une valeur de retour (Return of 'a) ou terminé en échec (Fail of exn).

Avertissement 1: Utilisation de Lwt_main.run

Dans les exemples de code qui suivent, Lwt_main.run est utilisé pour "évaluer" le thread en paramètre, c'est-à-dire faire reprendre les threads en attente d'un événement (comme une entrée/sortie). En pratique, dans un programme, on n'utilisera cette fonction que sur le thread "principal" du programme, qui crée les autres.

Dans l'exemple suivant, nous créons un tuyau (pipe) dont nous obtenons le canal de lecture ic et le canal d'écriture oc. Ensuite, nous créons un thread Lwt reader qui lit un caractère dans le pipe (et donc attend qu'un caractère soit disponible sur le canal de lecture). Nous affichons ensuite son état avec Lwt.State. Puis nous écrivons un caractère dans le pipe, et redemandons l'état du thread reader. Nous mettons le tout dans une fonction que nous passons à Lwt_main.run, sinon aucun thread en attente ne serait débloqué:

# let state_as_string thr =
  match Lwt.state thr with
  Lwt.Return c -> Printf.sprintf "Return with '%c'" c
| Lwt.Fail _ -> "Fail"
| Lwt.Sleep -> "Sleep"
;;
val state_as_string : char Lwt.t -> string = <fun>
# let run () =
  let ic, oc = Lwt_io.pipe () in
  let reader = Lwt_io.read_char ic in
  (* afficher l'état et attendre la fin de l'affichage avec >>= *)
  Lwt_io.write_line Lwt_io.stdout (state_as_string reader)
  >>= fun _ ->
    (* écrire dans le canal et ne pas attendre *)
    ignore(Lwt_io.write_char oc 'a');
    (* bloquer en attendant la terminaison de la lecture dans le canal *)
    reader >>= fun _ ->
      (* afficher le nouvel état du lecteur *)
      Lwt_io.write_line Lwt_io.stdout (state_as_string reader)
;;
val run : unit -> unit Lwt.t = <fun>
# Lwt_main.run (run ());;
Sleep
Return with 'a'
- : unit = ()

En pratique, on n'utilisera que rarement la possibilité de connaître l'état d'un thread Lwt. Par contre, on utilisera des fonctions agissant en fonction de l'état de threads Lwt, comme la fonction Lwt.join qui permet d'attendre la fin de plusieurs threads Lwt ou Lwt.choose qui retourne le même résultat que le thread Lwt qui termine en premier parmi une liste:

# let t = Lwt.join
  [ Lwt_io.write_line Lwt_io.stdout "message 1" ;
    Lwt_io.write_line Lwt_io.stdout "message 2" ;
  ] >>= fun _ -> Lwt_io.flush Lwt_io.stdout
  in
  Lwt_main.run t ;;
message 2
message 1
- : unit = ()
# Lwt_main.run (Lwt.choose  [ Lwt.return 0 ; Lwt.return 42 ; Lwt.return 56 ]) ;;
- : int = 56
2.4. Exercices
Exercice 1: Grep simplifié et multiplexé

Nous souhaitons écrire une version simplifiée mais multiplexée de grep. Le programme prendra en paramètre une chaîne et une liste de fichiers. Si la liste de fichiers est vide, l'entrée standard sera utilisée.

A l'aide des flux Lwt, on lira en parallèle les lignes des fichiers donnés et, pour chaque ligne, si elle contient la chaîne recherchée (utilisation du module Str, par exemple), on l'affichera sur la sortie standard.

Si plusieurs fichiers sont donnés, le nom de chaque fichier suivi de ':' sera affiché en tête de chaque ligne conservée.

Si le code est placé dans un fichier lwt_grep.ml, on pourra compiler le programme avec la commande suivante:

ocamlfind ocamlopt -o lwt-grep -package lwt.unix,str -linkpkg lwt_grep.ml
Exercice 2: Exécution de commandes dépendantes

Lwt permet de paralléliser les attentes non seulement sur les entrées/sorties mais également sur d'autres processus, comme l'illustre cet exercice.

Nous souhaitons exécuter des commandes shell, dont certaines sont dépendantes d'autres commandes et ne peuvent donc être exécutées qu'après ces dernières.

Plutôt qu'exécuter une à une les commandes après les avoir triées selon leurs dépendances, nous allons utiliser Lwt pour exécuter ces commandes le plus possible en parallèle.

Ecrire un module Lwt_commands offrant l'interface suivante:

type command

(** Création d'une commande à partir d'une chaîne (la commande à lancer)
  et des commandes dont elle dépend. *)
val mk_command : string -> command list -> command

(** Exécution de la commande et de ses dépendances, en utilisant
 le parallélisme latent dans les dépendances.
 Utilise Lwt.fail (Failure ...) en cas d'erreur d'exécution d'une commande. *)
val run : command -> unit Lwt.t

On utilisera notamment des fonctions des modules Lwt_process et Lwt_list, ainsi que Lwt.fail pour signaler une erreur.

Ce module pourra être compilé par les commandes suivantes:

ocamlfind ocamlopt -c -package lwt.unix lwt_commands.mli
ocamlfind ocamlopt -c -package lwt.unix lwt_commands.ml

On pourra tester le module avec le code qui suit, ce dernier créant plusieurs commandes. clean permet de s'assurer que les fichiers file1.txt, file2.txt et file3.txt sont supprimés. La commande principale lance un ls -l sur ces fichiers et échouera donc s'ils n'existent pas. Elle dépend donc de trois autres commandes, chacune dormant une seconde avant de créer un fichier vide. Ces trois commandes dépendent également chacune de la commande clean:

let com =
  let clean = Lwt_commands.mk_command "rm -f file1.txt file2.txt file3.txt" [] in
  Lwt_commands.mk_command "ls -l file*.txt"
    [ Lwt_commands.mk_command "sleep 1 ; touch file1.txt" [ clean ] ;
      Lwt_commands.mk_command "sleep 1 ; touch file2.txt" [ clean ] ;
      Lwt_commands.mk_command "sleep 1 ; touch file3.txt" [ clean ] ;
    ]

let () =
  try Lwt_main.run (Lwt_commands.run com)
  with Failure msg -> prerr_endline msg ; exit 1

En plaçant le code ci-dessus dans un fichier lwt_commands_test.ml et en compilant le tout dans un programme lwt-commands avec la commande suivante:

ocamlfind ocamlopt -o lwt-commands -package lwt.unix -linkpkg \
  lwt_commands.cmx lwt_commands_test.ml

nous pouvons vérifier que l'exécution de notre programme parallélise bien les trois commandes qui créent les trois fichiers, puisque le temps d'exécution total est d'à peine plus d'une seconde, et non trois si les commandes de création avaient été exécutées séquentiellement:

/usr/bin/time -f %E ./lwt-commands
-rw-r--r-- 1 guesdon guesdon 0 déc.   7 22:35 file1.txt
-rw-r--r-- 1 guesdon guesdon 0 déc.   7 22:35 file2.txt
-rw-r--r-- 1 guesdon guesdon 0 déc.   7 22:35 file3.txt
0:01.00
2.5. Conclusion

Nous n'avons vu qu'un ensemble restreint des possibilités offertes par Lwt. En particulier, nous n'avons pas évoqué la possibilité d'annuler un thread Lwt, la gestion des exceptions, les variables locales à un thread Lwt.

Si l'usage de Lwt présente d'incontestables facilités pour écrire du code fortement soumis à des attentes longues6, il présente un inconvénient: les codes qui utilisent une bibliothèque s'appuyant sur Lwt ont tendance à être "contaminés", les résultats fournis par la bibliothèque en question étant empaquetés dans les threads Lwt, ce qui pousse à propager la logique de parallélisation des attentes.

Symétriquement, dans un code s'appuyant sur Lwt mais souhaitant interagir avec d'autres bibliothèques n'utilisant pas Lwt, il conviendra de s'assurer qu'aucune opération longue de ces bibliothèques ne vient bloquer la parallélisation des attentes offertes par Lwt. Si c'est le cas, on exécutera les parties potentiellement longues ou bloquantes dans un thread système séparé grâce à la fonction Lwt_preemptive.detach (et on compilera en utilisant en plus le paquet lwt.preemptive).

Plusieurs bibliothèques OCaml utilisent Lwt ou offrent une partie de leur interface "en Lwt", notamment pour faciliter l'implémentation de communication via le réseau, par exemple cohttp ou ocurl.

2.6. Async

La bibliothèque Async est une alternative à Lwt, offrant une interface similaire: empaquetage des valeurs dans un type 'a Deferred.t, opérations bind et return (bref, une monade).

On pourra lire ce chapitre (en anglais) de [5] pour une présentation et des exemples détaillés.

Les effets de contagion du modèle basé sur la monade, signalés pour Lwt, sont également présents avec Async.

3. Processus légers

Le but de cette introduction n'est pas de présenter ce que sont les processus légers. On pourra se référer par exemple à [6].

Les processus légers sont gérés à l'aide du module Thread. Ils sont disponibles sur les plateformes Posix 1003.1c et Win32.

La compilation de modules utilisant les threads nécessite de passer l'option -thread aux compilateurs ocamlc et ocamlopt, ainsi que de lier avec la bibliothèque threads.cma (pour le code-octet) ou threads.cmxa (pour le code natif). Comme cette bibliothèque nécessite également la bibliothèque Unix, on liera le programme final avec aussi unix.cma ou unix.cmxa.

Avertissement 2: Threads natifs ou émulés

Si le système pour lequel est compilé le code utilisant des threads ne supporte pas les threads, il faut remplacer l'option -thread par l'option -vmthread. Tous les threads sont alors exécutés dans le même processus. Cette option n'est cependant disponible que pour la compilation en code-octet.

Exercice 3: Affichages en parallèle

Un exercice classique d'utilisation des threads est l'affichage de messages en parallèle. Ecrire un programme créant 3 threads, qui affiche chacun 10 lignes de la forme "Je suis le thread X et c'est mon affichage N.".

Attention à bien attendre la fin des threads, sinon le programme principal termine, terminant également les threads avant qu'ils aient fini leur travail.

Si le code se trouve dans un fichier thread_print.ml, on pourra le compiler par la commande suivante:

$ ocamlopt -o thread_print -thread unix.cmxa threads.cmxa thread_print.ml

En exercice supplémentaire, on pourra utiliser un mutex pour s'assurer du non entrelacement des sorties.

Différents modules sont également disponibles pour la communication entre threads:

  • Mutex pour la gestion des sections critiques (exclusion mutuelle),
  • Condition pour attendre ou signaler une condition,
  • Event pour une programmation basée sur des événements envoyés sur des canaux.
4. Processus lourds

La majorité des bibliothèques permettant la parallélisation utilise des processus lourds, exécutés soit en local, soit à distance en se connectant à d'autres machines pour y créer des processus. Cette organisation des calculs nécessite donc des copies de données.

OCaml, via le module Unix, offre l'accès aux appels systèmes permettant la création de processus (fork), la communication via des pipes et des sockets, etc. On lira utilement [6].

Il existe cependant plusieurs bibliothèques offrant des abstractions pour faciliter ces modèles de calcul parallèle et/ou distribué:

  • Async-parallel, une extension d'Async pour l'exécution de code en parallèle sur des machines distantes,
  • Parmap permet l'exécution de fonctions sur plusieurs cœurs,
  • OCamlnet, une bibliothèque offrant beaucoup de fonctions pour développer des applications utilisant le réseau, contient plusieurs modules pour la programmation parallèle ainsi que pour le partage de structures de données et le passage de messages,
  • OCamlMPI est une interface avec la bibliothèque de passage de messages MPI,
  • Functory est une bibliothèque permettant d'effectuer des calculs séquentiellement ou en parallèle, soit sur la même machine, soit sur des machines distantes.
4.1. Functory

Functory présente l'avantage d'avoir une interface commune pour les différentes façons de distribuer les calculs à effectuer, ainsi qu'un module pour l'exécution séquentielle, facilitant le débogage et/ou l'obtention d'un comportement de référence.

En guise d'exemple, nous allons écrire un programme qui applique une fonction à différentes listes d'entiers, la première fois de façon séquentielle, la seconde en utilisant plusieurs processus locaux.

Nous définissons tout d'abord nos listes d'entiers:

# let input =
  [
    [ 1 ; 2 ; 3 ; 4 ; 5 ; 6 ; 7 ; 8 ; 9 ; 10 ] ;
    [ 2 ; 4 ; 6 ; 8 ; 10 ] ;
    [ -1 ; -7 ; -9 ; -42 ] ;
  ]
;;
val input : int list list =
  [[1; 2; 3; 4; 5; 6; 7; 8; 9; 10]; [2; 4; 6; 8; 10]; [-1; ...]; ...]

Nous définissons également une fonction sommant une liste d'entiers en paramètre:

# let sum = List.fold_left (+) 0;;
val sum : int list -> int = <fun>

Enfin, nous utilisons le module de la bibliothèque Functory pour effectuer les opérations séquentiellement, afin d'appliquer la fonction sum sur chacune de nos listes:

# Functory.Sequential.map sum input ;;
- : int list = [55; 30; -59]

Si nous souhaitons effectuer le même traitement, mais en utilisant plusieurs cœurs de notre machine, il nous suffit d'indiquer le nombre maximum de cœurs à utiliser et d'appeler la fonction map du module Functory.Cores:

# Functory.Cores.set_number_of_cores 4;;
- : unit = ()
# Functory.Cores.map sum input ;;
- : int list = [55; 30; -59]

On peut ainsi, en changeant par exemple un open, utiliser soit le map séquentiel, soit le map parallèle local.

De la même façon, il est possible de lancer les calculs sur des machines distantes, préalablement déclarées, à l'aide du module Functory.Network. Il y a plusieurs modules, selon que le programme distant sera le même, ou bien qu'il sera différent mais compilé avec la même version d'OCaml, ou encore différent et compilé avec une autre version d'OCaml. Dans l'exemple ci-dessous, nous utilisons le module Same car le programme distant est le même que celui local.

Functory.Network.declare_workers ~n: 4 "machine1";;
Functory.Network.declare_workers ~n: 8 "machine2";;
Functory.Network.Same.map sum input ;;

Les programmes sur les machines distantes doivent être lancés et attendre les instructions du maître avec le code suivant:

Functory.Network.Same.Worker.compute ();;

Bien sûr, les calculs à répartir sont en général un peu plus gourmands en temps et/ou en mémoire que notre fonction jouet...

La bibliothèque offre d'autres fonctions que map pour effectuer des calculs (différentes variantes de fold), la possibilité de paramétrer le port par lequel maître et esclaves communiquent, ... On consultera la documentation pour en savoir davantage.

4.2. Parmap

Parmap offre seulement la possibilité d'utiliser plusieurs cœurs de la machine, mais offre davantage de fonctions, traitant à la fois les listes et les tableaux.

Avec notre liste de listes input et notre fonction sum précédentes, nous pouvons appliquer sum à chacune des listes de la façon suivante, en utilisant 6 cœurs:

# Parmap.parmap ~ncores: 6 sum (Parmap.L input);;
- : int list = [55; 30; -59]

On consultera avantageusement la documentation de Parmap pour découvrir les fonctions offertes pour paralléliser des calculs.

5. Conclusion

Il existe plusieurs façons de paralléliser des calculs et gérer la concurrence, chacune pouvant être basée sur des bibliothèques variées.

Pour des développements basés sur le modèle à monades évoqué ci-dessus et utilisant peu d'autres bibliothèques, le choix reste assez ouvert. Cependant, lorsqu'il s'agit de composer plusieurs bibliothèques existantes, on s'assurera de leur compatibilité entre elles. En effet, plusieurs bibliothèques utilisent Lwt, tandis que d'autres se basent sur Async, d'autres n'utilisent ni l'une ni l'autre et d'autres enfin fournissent des implémentations pour les deux.

Le choix du modèle de parallélisme sort du cadre de cette introduction. On signalera simplement qu'il est orienté par certaines contraintes comme la taille des données manipulées (en cas de copie, transfert, ...), la disponibilité ou non de noeuds de calculs, distribués ou non.

La programmation purement fonctionnelle facilite le développement de codes de calcul parallèle. OCaml est donc naturellement un bon choix pour ce style de programmation.


1 On pourra lire à ce sujet ce billet de Robert Harper.
2 Un autre problème est que pour l'instant certaines parties du runtime ne sont pas réentrantes.
3 Il existe également Jocaml que nous ne présenterons pas ici car il s'agit d'une extension d'OCaml.
4 Si on ne connaît pas les monades, on pourra lire les trois billets d'introduction par Bartosz Milewski: 1, 2, 3.
5 En utilisant des sélecteurs sur les descripteurs de fichiers concernés. Voir la documentation pour plus de détails.
6 Lwt est notamment utilisé par Ocsigen, un serveur web en OCaml qui par nature passe du temps à attendre et envoyer des messages sur le réseau.