L'objectif de ce TD est de manipuler les threads en OCaml. Leur
utilisation principale est de pouvoir gérer les
opérations bloquantes. Par exemple, la
fonction Unix.read
qui permet de lire des données sur un
réseau reste en attente tant que des données n'ont pas été reçues. Il est
donc nécessaire de faire de telles lectures dans des threads séparés afin
de ne pas bloquer tout le programme.
Nous allons commencer par un exemple basique qui montre certains phénomènes liés à l'utilisation des threads. Pour pouvoir les utiliser, il faut modifier les propriétés du projet:
unix,threads
-thread
-thread
Dans un fichier incr.ml, définir la fonction f
suivante, qui incrémente 10000 fois un compteur en affichant à chaque fois
sa valeur.
let n = ref 0 let f th = for i = 1 to 10000 do let x = !n in Printf.printf "%d: %d\n%!" th x; n := x + 1; doneComplétez le programe afin de
f
avec comme argument le
numéro du thread : on utilisera la
fonction Thread.create
de type
('a -> 'b) -> 'a -> t
, qui lance une fonction appliquée à
un argument dans un nouveau thread (qui est créé et lancé
immédiatement), et on stockera les threads dans un tableau,
Thead.join
qui attend qu'un thread se termine,
n
.
n
en fin de programme
? Quelle valeur obtenez-vous ? Cette valeur est-elle toujours la même
?
n
n'aura pas changé entre le moment où on
lit sa valeur dans x
et celui où on
incrémente n
. Les fonctions utiles
seront Mutex.create
pour créer un
mutex, Mutex.lock
pour prendre un mutex
et Mutex.unlock
pour le relâcher : on rappelle qu'à tout instant, seul un thread peut
avoir pris un mutex. Vérifiez que la valeur finale pour n
est bien 100000 sur plusieurs exécutions.
Dans la suite, on s'efforcera de systématiquement protéger par des mutex toute référence (ou plus généralement toute structure de données muable) qui peut être accédée de façon concurrente par deux threads, dont l'un cherche à écrire (deux lectures concurrentes n'ont pas à être protégées).
Déposez votre fichier :
Commencez par récupérer le fichier network.ml qui est un module qui encapsule les opérations sur le réseau. Il n'est pas nécessaire que vous compreniez les détails de son implémentation.
Le fichier client.ml contient un navigateur web (très) basique. Tout d'abord, il se connecte au site www.polytechnique.edu sur le port 80 (c'est le port utilisé par défaut par le protocole HTTP), la valeur cnx correspond à cette connexion. Ensuite il envoie la requête
GET /index.php HTTP/1.1 host: www.polytechnique.eduqui signifie qu'il demande la page index.php en utilisant la version 1.1 du protocole HTTP, sur l'hôte www.polytechnique.edu. Puis il lit la réponse et l'affiche sur la sortie standard.
Une remarque : en salle machine, n'essayez pas de changer le site www.polytechnique.edu par un autre, cela ne fonctionnerait pas à cause du proxy.
input_line stdin
(cette fonction ne renvoie pas le retour à la ligne final,
il faut donc le rajouter), puis l'écrit sur la connexion,
Network.read
renvoie une chaîne de caractères vide. Pour
tester, il suffit d'envoyer une requête incorrecte suivie de deux
retours à la ligne, par exemple:
blop(qui n'est pas une requête valide du protocole HTTP), ce à quoi le serveur web répond
HTTP/1.1 400 Bad Request Server: SysDSI/6.6.6 Date: Tue, 02 May 2017 15:47:27 GMT Content-Type: text/html Content-Length: 173 Connection: close <html> <head><title>400 Bad Request</title></head> <body bgcolor="white"> <center><h1>400 Bad Request</h1></center> <hr><center>SysDSI/6.6.6</center> </body> </html>avant de fermer la connexion.
Déposez votre fichier :
Après le client, passons au serveur web ! Vous pouvez partir du fichier server.ml. Ce serveur écoute sur le port 8080 de la machine locale. À chaque demande de page, le serveur répond
HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 9 Bonjour !ce qui signfie qu'il renvoie un fichier texte qui contient Bonjour !, quelle que soit l'url. Vous pouvez par exemple, regarder les pages http://localhost:8080/ ou http://localhost:8080/blabla. Ici, localhost désigne la machine locale (qui correspond à l'IP 127.0.0.1).
Unix.sleep 1;dans le traitement des requêtes pour simuler un long calcul ou un accès coûteux au disque (qui bloquerait le thread courant pendant une seconde), puis lancez la commande
ab -n 9 -c 3 http://127.0.0.1:8080/qui simule 9 téléchargements de pages, dont 3 se font de façon concurrente (le programme ab permet d'effctuer divers tests sur des serveurs web). Comparez entre une implémentation avec et sans threads le temps total (Time taken for tests) et le nombre de requête abouties par seconde (Requests per second) : on pourra passer simplement de l'une à l'autre des implémentations en remplaçant le lancement de thread par un appel de fonction.
La factorielle de 9 est 362880.Bien sûr cela devra aussi fonctionner pour tout nombre autre que 9.
La page a été vue NNN fois.où NNN est le nombre de fois où cette page a été vue depuis le lancement du serveur. Doit-on utiliser un mutex ? Si oui, faites-le.
GET /bonjour.txt HTTP/1.1Écrivez une une fonction
get_file : string -> string
qui
extrait la chaîne de caractères bonjour.txt de la requête. On
aura ainsi
# get_file "GET /bonjour.txt HTTP/1.1...";; - : string = "bonjour.txt"On pourra utiliser les fonctions du module String comme
String.index_from
pour trouver la position de
l'espace suivant le nom de fichier et String.sub
pour
extraire le nom du fichier.
let () = (* On crée le buffer qu'on va utiliser pour lire le fichier. *) let buflen = 1024 in let buf = Bytes.create buflen in let loop = ref true in (* On ouvre le fichier. *) let f = Unix.openfile "bonjour.txt" [Unix.O_RDONLY] 0o644 in while !loop do (* On lit des données dans le fichier. *) let n = Unix.read f buf 0 buflen in (* On écrit ce qu'on a lu sur la sortie standard. *) assert (Unix.write Unix.stdout buf 0 n = n); (* Si on a lu 0 octets alors on est en fin de fichier. *) if n = 0 then loop := false done; (* On ferme le fichier. *) Unix.close fDe plus
Unix.stat
permet d'obenir la taille d'un fichier
(que le serveur doit renvoyer dans le champ Content-Length) :
# (Unix.stat "bonjour.txt").Unix.st_size;; - : int = 30Essayez aussi de vous connecter au serveur de votre voisin en remplaçant localhost par le nom de sa machine dans l'URL.
HTTP/1.1 404 Not Foundau lieu de
HTTP/1.1 200 OKsuivie d'un message d'erreur.
Déposez votre fichier :
À chaque requête, un thread système est créé par notre serveur, ce qui se révèle trop coûteux s'il y a un grand nombre de connexions simultanées. Afin de parer à cette difficulté la librairie lwt a été créée pour fournir des threads légers (lwt = lightweight threads) qui peuvent être lancés en très grand nombre (celle-ci a été initialement créée pour implémenter le serveur web ocsigen).
Afin de pouvoir utiliser la librairie lwt, nous allons utiliser opam qui permet de télécharger et compiler automatiquement des librairies OCaml. Si vous ne l'avez pas déjà fait, tapez les commandes suivantes dans un terminal permettant d'utiliser opam (elles prennent quelques minutes) :
# initialisation d'opam (répondez oui à toutes les questions) opam init # déclaration des variables d'environement eval `opam config env`puis installez lwt :
# installation de lwt opam install lwt # fermez eclipse puis relancez-le depuis cette console (c'est important) eclipse&
Nous allons maintenant configurer Eclipse pour qu'il utilise notre OCaml fraichement installé.
/HOME/.opam/system/liboù /HOME est l'adresse de votre home (vous pouvez l'obtenir en tapant pwd dans une console) et cliquez sur ok.
-package lwt.unix
Ouf, nous pouvons maintenant commencer à travailler...
Lwt_io
(au lieu de la librairie standard d'OCaml comme les
modules Printf
ou Unix
). Le seconde c'est que
pour lancer un thread lwt il faut
utiliser Lwt_main.run
. Testez
le programme suivant, dans un fichier bonjour.ml qui affiche
bonjour :
let f () = Lwt_io.print "Bonjour\n" let () = Lwt_main.run (f ())
let bonjour nom = Lwt_io.print ("Bonjour " ^ nom ^ " !\n")et la lecture du prénom sera faite à l'aide de
Lwt_io.read_line Lwt_io.stdinEssayez d'écrire le programme demandé. Quel est le problème ?
# let bonjour nom = Lwt_io.print ("Bonjour " ^ nom ^ " !\n");; val bonjour : string -> unit Lwt.t = <fun> # Lwt_io.read_line Lwt_io.stdin;; - : string Lwt.t = <abstr>Par exemple le type
string Lwt.t
indique un thread lwt qui
renvoie une valeur de type string
. Essayez de les combiner en
utilisant la
fonction Lwt.bind
dont le type est le suivant :
# Lwt.bind;; - : 'a Lwt.t -> ('a -> 'b Lwt.t) -> 'b Lwt.t = <fun>
>>=
défini dans le
module Lwt
: plutôt que d'écrire
Lwt.bind a fon préférera écrire
a >>= faprès avoir préalablement déclaré l'opérateur
>>=
par
open LwtModifiez le programme précédent afin d'utiliser
>>=
au
lieu de Lwt.bind
.
Warning 10: this expression should have type unit.utilisez
>>=
afin de le supprimer.
Déposez votre fichier :
À titre d'exercice, nous allons maintenant « convertir » le programme de la première question (qui lance 10 threads qui incrémentent un compteur) afin d'utiliser lwt au lieu de la librairie standard (pour le threads et les entrées/sorties). Vous repartirez de votre fichier incr.ml, puis appliquerez la recette suivante :
Lwt_io.printf
au lieu
de Printf.printf
,
Lwt.bind
pour faire en sorte que le programme
type : la règle générale est qu'il faut transformer un code de la
forme
let x = f y in gdans lequel
x
a un type de la forme 'a Lwt.t
en
f y >>= fun x -> gil est aussi parfois nécessaire d'utiliser la fonction
Lwt.return
de type
# Lwt.return;; - : 'a -> 'a Lwt.t = <fun>qui transforme une valeur en un thread lwt qui renvoie cette valeur,
Lwt.join
dont le type est
# Lwt.join;; - : unit Lwt.t list -> unit Lwt.t = <fun>
Lwt_main.run
.
Déposez votre fichier :
Finalement, convertissez le code de votre serveur afin d'utiliser
lwt. Vous repartirez du fichier server.ml que vous avez écrit à
la question précédente et appliquerez la recette ci-dessus et utiliserez
de plus le module Lwt_unix
au lieu du
module Unix
(les fonctions portent le même nom dans les deux
modules).
Déposez votre fichier :