• Aucun résultat trouvé

Exemple complet : le crible d’´ Eratosth`ene parall`ele

L’exemple qui suit est un grand classique de la programmation par processus communicants. Le but du programme est d’´enum´erer les nombres premiers et de les afficher au fur et `a mesure. L’id´ee est la suivante : initialement, on connecte un processus qui ´enum`ere les entiers `a partir de 2 sur sa sortie avec un processus “filtre”. Le processus filtre commence par lire un entier p sur son entr´ee, et l’affiche `a l’´ecran.

g´en´erateur d’entiers

2,3,4, . . .

lire p

Le premier processus filtre lit donc p = 2. Ensuite, il cr´ee un nouveau processus filtre, connect´e `a sa sortie, et il se met `a filtrer les multiples de p depuis son entr´ee ; tous les nombres lus qui ne sont pas multiples de p sont r´e´emis sur sa sortie.

g´en´erateur d’entiers 2,3,4, . . . filtre les 2n 3,5,7, . . . lire p

Le processus suivant lit donc p = 3, l’affiche, et se met `a filtrer les multiples de 3. Et ainsi de suite. g´en´erateur d’entiers 2,3,4, . . . filtre les 2n 3,5,7, . . . filtre les 3n 5,7,11, . . . . . . lire p

Cet algorithme n’est pas directement impl´ementable en Unix, parce qu’il cr´ee trop de proces- sus (le nombre d’entiers premiers d´ej`a trouv´es, plus un). La plupart des syst`emes Unix limitent le nombre de processus `a quelques dizaines. De plus, dix processus actifs simultan´ement suffisent `

a effondrer la plupart des machines mono-processeurs, en raison des coˆuts ´elev´es de commuta- tion de contextes pour passer d’un processus `a un autre. Dans l’impl´ementation qui suit, chaque processus attend d’avoir lu un certain nombre d’entiers premiers p1, . . . , pk sur son entr´ee avant

de se transformer en filtre `a ´eliminer les multiples de p1, . . . , pk. En pratique, k = 1000 ralentit

raisonnablement la cr´eation de nouveaux processus.

On commence par le processus qui produit les nombres entiers de 2 `a k.

1 open Unix;; 2

3 let input_int = input_binary_int 4 let output_int = output_binary_int 5

6 let generate k output = 7 let rec gen m =

8 output_int output m; 9 if m < k then gen (m+1) 10 in gen 2;;

Pour communiquer les entiers en la sortie d’un g´en´erateur et l’entr´ee du filtre suivant on peut utiliser les fonctions suivantes :

La fonction output_binary_int est une fonction de la biblioth`eque standard qui ´ecrit la repr´esentation d’un entier (sous forme de quatre octets) sur un out_channel. L’entier peut ensuite ˆetre relu par la fonction input_binary_int. L’int´erˆet d’employer ici la biblioth`eque standard est double : premi`erement, il n’y a pas `a faire soi-mˆeme les fonctions de conversion entiers/chaˆınes de quatre caract`eres (la repr´esentation n’est pas sp´ecifi´ee, mais il est garanti que pour une version du langage, elle est ind´ependante de la machine) ; deuxi`emement, on fait beaucoup moins d’appels syst`eme, car les entr´ees/sorties sont temporis´ees, d’o`u de meilleures performances. Les deux fonctions ci-dessous construisent un in_channel ou un out_channel qui temporise les lectures ou les ´ecritures sur le descripteur Unix donn´e en argument :

val in_channel_of_descr : file_descr -> in_channel val out_channel_of_descr : file_descr -> out_channel

Ces fonctions permettent de faire des entr´ees/sorties temporis´ees sur des descripteurs obtenus indirectement ou autrement que par une ouverture de fichier. Leur utilisation n’a pas pour but de m´elanger les entr´ees/sorties temporis´es avec des entr´ees sorties non temporis´ees, ce qui est possible mais tr`es d´elicat et fortement d´econseill´e, en particulier pour les lectures. Il est ´egalement possible mais tr`es risqu´e de construire plusieurs in_channel (par exemple) sur le mˆeme descripteur de fichier.

On passe maintenant au processus filtre. Il utilise la fonction auxiliaire read_first_primes. L’appel read_first_primes input n lit n nombres sur input (un in_channel), en ´eliminant les multiples des nombres d´ej`a lus. On affiche ces n nombres au fur et `a mesure de leur construc- tion, et on en renvoie la liste.

11 let print_prime n = print_int n; print_newline() 12

13 let read_first_primes input count =

14 let rec read_primes first_primes count = 15 if count <= 0 then

16 first_primes

17 else

18 let n = input_int input in

19 if List.exists (fun m -> n mod m = 0) first_primes then 20 read_primes first_primes count

21 else begin

22 print_prime n;

23 read_primes (n :: first_primes) (count - 1)

24 end in

25 read_primes [] count;;

Voici la fonction filtre proprement dite.

26 let rec filter input =

27 try

28 let first_primes = read_first_primes input 1000 in 29 let (fd_in, fd_out) = pipe() in

30 match fork() with 31 0 ->

32 close fd_out;

33 filter (in_channel_of_descr fd_in) 34 | p ->

35 close fd_in;

36 let output = out_channel_of_descr fd_out in 37 while true do

38 let n = input_int input in

39 if List.exists (fun m -> n mod m = 0) first_primes then () 40 else output_int output n

41 done

42 with End_of_file -> ();;

Le filtre commence par appeler read_first_primes pour lire les 1000 premiers nombres pre- miers sur son entr´ee (le param`etre input, de type in_channel). Ensuite, on cr´ee un tuyau et on clone le processus par fork. Le processus fils se met `a filtrer de mˆeme ce qui sort du tuyau. Le processus p`ere lit des nombres sur son entr´ee, et les envoie dans le tuyau s’ils ne sont pas multiples d’un des 1000 nombres premiers lus initialement.

Le programme principal se r´eduit `a connecter par un tuyau le g´en´erateur d’entiers avec un premier processus filtre (crible k ´enum`ere les nombres premiers plus petits que k. Si k est omis (ou n’est pas un entier) il prend la valeur par d´efaut max_int).

43 let crible () =

44 let len = try int_of_string Sys.argv.(1) with _ -> max_int in 45 let (fd_in, fd_out) = pipe () in

46 match fork() with 47 0 ->

48 close fd_out;

49 filter (in_channel_of_descr fd_in) 50 | p ->

51 close fd_in;

52 generate len (out_channel_of_descr fd_out);; 53

54 handle_unix_error crible ();;

Ici, nous n’avons pas attendu que les fils terminent pour terminer le programme. Les processus p`eres sont des g´en´erateurs pour leurs fils. Lorsque k est donn´e, le p`ere va s’arrˆeter en premier, et fermer le descripteur sur le tuyau lui permettant de communiquer avec son fils. Lorsqu’il termine, OCaml vide les tampons sur les descripteurs ouverts en ´ecriture, donc le processus fils peut lire les derni`eres donn´ees fournies par le p`ere. Il s’arrˆete `a son tour, etc. Les fils deviennent donc orphelins et sont temporairement rattach´es au processus 1 avant de terminer `a leur tour. Lorsque k n’est pas fourni, tous les processus continuent ind´efiniment (jusqu’`a ce que l’un ou plusieurs soient tu´es). La mort d’un processus entraˆıne la terminaison d’un de ses fils comme dans le cas pr´ec´edent et la fermeture en lecture du tuyau qui le relie `a son p`ere. Cela provoquera la terminaison de son p`ere `a la prochaine ´ecriture sur le tuyau (le p`ere recevra un signal SIGPIPE, dont l’action par d´efaut est la terminaison du processus).

Exercice 12 Comment faut-il modifier le programme pour que le p`ere attende la terminaison

de ses fils ? (Voir le corrig´e)

Exercice 13 `A chaque nombre premier trouv´e, la fonction print_prime ex´ecute l’expression print_newline()qui effectue un appel syst`eme pour vider de tampon de la sortie standard, ce qui limite artificiellement la vitesse d’ex´ecution. En fait print_newline() ex´ecute print_char ’\n’ suivi de flush Pervasives.stdout. Que peut-il se passer si on ex´ecute simplement print_char ’\n’? Que faire alors ? (Voir le corrig´e)