Introduction au langage JR

Baptiste Wicht

Cet article va vous présenter le langage de programmation JR et les concepts de base qu'il propose.

Commentez ♪

Version anglophone de cet article - English version of this article: Introduction to the JR programming language

- Titre : Introduction au langage JR

- Auteur : Baptiste Wicht

- Parution : 24 janvier 2010

- Mise à jour : 24 janvier 2010

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2009 Baptiste Wicht. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- 1. Présentation du langage

- 2. Hello World

- 3. Processus

- 4. Quiescence action

- 5. Sémaphores

- 6. Conclusion

- 6-1. Remerciements

1 - Présentation du langage

JR est un langage de programmation créé spécialement pour résoudre des problèmes de programmation concurrente. Ce langage est en fait une surcouche de Java qui ajoute à ce dernier les principaux paradigmes de programmation concurrente. En plus de cela, JR facilite aussi la gestion des concepts déjà implémentés dans Java tels que les processus ou les sémaphores. Il existe également des extensions pour JR permettant d'implémenter d'autres fonctionnalités telles que les moniteurs ou les Conditional Critical Region (CCR). JR est en fait l'implémentation du langage SR pour Java.
En fait, JR ne fait rien d'autre que rajouter une couche au-dessus de Java, une fois qu'on utilise le compilateur JR, les fichiers sources JR sont transformés en fichiers Java et sont exécutés par la machine virtuelle Java comme n'importe quelle classe Java.

JR est surtout utilisé comme support scolaire pour apprendre la programmation concurrente.

Dans cet article, nous allons voir les bases de la programmation avec JR.

La version présentée ici est celle de juin 2009, la version 2.00602 qui se base sur Java 6.0.

Cet article suppose que vous avez installé l'environnement JR sur votre machine. Un article est disponible ici pour l'installation de l'environnement JR sous Windows.

Dans cet article, nous allons va surtout s'intéresser aux apports du langage JR pour ce qui est de la programmation concurrente, nous n'allons donc pas voir l'intégralité du langage, car en plus de fournir des facilités de programmation concurrente, ce langage propose également d'autres concepts que nous ne verrons pas ici. De plus, tous les aspects de la programmation concurrente ne sont pas traités dans cet article, seuls les concepts de base seront vus.

2 - Hello World

Comme pour tous les langages, il est essentiel de commencer avec un simple Hello World. Nous allons donc créer un fichier Hello.jr. Rien de spécial de ce côté-là, c'est du Java pur et simple :

Hello.jr

public class Hello {
public static void main(String[] args){
System.out.println("Hello World");
}
}

Nous allons ensuite compiler ce programme :

jrc Hello.jr

Cela va créer un dossier jrGen contenant des fichiers Java. Le résultat d'une compilation JR est toujours un ensemble de fichiers Java correspondant à la traduction du programme JR.

Pour lancer votre programme Java, utilisez la commande jr suivie du nom de la classe contenant la méthode main :

jr Hello

Ce qui affichera :

Hello World

Rien de spécial donc. La commande jr va également lancer la compilation des fichiers Java. Cette compilation se fera à tous les coups. Si vous voulez effectuer seulement le lancement des fichiers déjà compilés, vous pouvez utiliser la commande jrrun.

Comme dit en introduction, le langage JR étend le langage Java, ce qui fait qu'il est possible de coder en Java avec JR. Un Hello World est donc seulement du Java.

3 - Processus

La première chose à voir est évidemment la déclaration des processus. Cela se fait de manière bien plus simple qu'en Java. Pas besoin d'instancier des objets, cela se fait de manière déclarative et c'est JR qui se charge du reste.

Pour la déclaration des processus, JR introduit un nouveau mot clé process qui permet de déclarer un processus. Voici la déclaration la plus simple qui soit d'un thread :

process Hello {
System.out.println("Processus");
}

Comme vous pouvez le constater, c'est bien plus simple qu'en Java. Et encore mieux, même pas besoin de le lancer, il suffit d'instancier la classe. Un thread peut également être déclaré static. Cette fois-ci, il ne sera pas lancé lors de l'instanciation de la classe, mais directement dès sa résolution par la machine virtuelle. Par exemple, nous pourrions réécrire un HelloWorld de cette manière :

public class HelloProcess {
static process Hello {
System.out.println("Hello World");
}

public static void main(String[] args){}
}

qui va afficher exactement la même chose que notre première version d'Hello World. À la différence près que notre affichage se fera depuis un thread.

En plus de cela, JR permet également de déclarer une grosse série de threads en une déclaration avec la syntaxe suivante :

static process Hello((int id = 0; id < n; id++)){}

Cela va déclarer n threads. La syntaxe est la même que pour la boucle for. Déclarons 25 threads Hello World :

HelloProcess.jr

public class HelloProcess {
static process Hello((int id = 0; id < 25; id++)){
System.out.println("Hello World from thread " + id);
}

public static void main(String[] args){}
}

Voici ce qui devrait s'afficher :

Hello World from thread 2
Hello World from thread 24
Hello World from thread 11
Hello World from thread 22
Hello World from thread 0
Hello World from thread 4
Hello World from thread 6
Hello World from thread 8
Hello World from thread 10
Hello World from thread 12
Hello World from thread 14
Hello World from thread 16
Hello World from thread 18
Hello World from thread 20
Hello World from thread 23
Hello World from thread 21
Hello World from thread 19
Hello World from thread 17
Hello World from thread 15
Hello World from thread 13
Hello World from thread 9
Hello World from thread 7
Hello World from thread 5
Hello World from thread 3
Hello World from thread 1

Comme vous pouvez le voir si vous lancez plusieurs fois le programme, les threads peuvent être lancés dans un ordre complètement différent à chaque lancement. Rien ne peut garantir l'ordre de lancement des threads et il ne faut absolument pas compter dessus. C'est d'ailleurs la base de la programmation concurrente de ne pas pouvoir prédire l'ordre d'exécution des instructions dans les différents threads.

4 - Quiescence action

JR introduit un concept très puissant, la « quiescence action ». C'est une action qui est exécutée lorsque le système est en état dit de quiescence. C'est-à-dire que soit tous les processus sont terminés ou alors tous les processus sont en état de deadlock.

Avant cela, il nous faut juste introduire le concept d'opérations. Dans notre cas, une opération est une simple méthode déclarée avec le préfixe op. En fait, en JR une opération est plus que cela et peut être invoquée de différentes manières et permet d'autres choses que de simples méthodes Java, mais cela dépasse le cadre de cet article.

Voici la déclaration d'une opération :

public static op void fin(){
System.out.println("Fin");
}

C'est donc une simple méthode avec le préfixe op. Vous pouvez d'ailleurs l'appeler comme n'importe quelle autre méthode. Mais vous pouvez aussi la déclarer comme étant l'action à exécuter lorsque le système arrive en état de quiescence. Si nous reprenons notre exemple des 25 Hello World et que nous définissions la quiescence action voici ce que ça pourrait donner :

import edu.ucdavis.jr.JR;

public class QuiescenceProcess {
static process Hello((int id = 0; id < 25; id++)){
System.out.println("Hello World from thread " + id);
}

public static void main(String[] args){
try {
JR.registerQuiescenceAction(fin);
} catch (edu.ucdavis.jr.QuiescenceRegistrationException e){
e.printStackTrace();
}
}

public static op void fin(){
System.out.println("Fin");
}
}

Nous utilisons donc la méthode registerQuiescenceAction(op) de la classe JR qui fournit quelques méthodes utilitaires pour les programmes JR.
Et au lancement, nous pourrons constater quelque chose comme ceci :

Hello World from thread 0
Hello World from thread 22
Hello World from thread 23
Hello World from thread 21
Hello World from thread 24
Hello World from thread 20
Hello World from thread 19
Hello World from thread 18
Hello World from thread 17
Hello World from thread 16
Hello World from thread 15
Hello World from thread 14
Hello World from thread 13
Hello World from thread 12
Hello World from thread 11
Hello World from thread 10
Hello World from thread 9
Hello World from thread 8
Hello World from thread 7
Hello World from thread 6
Hello World from thread 5
Hello World from thread 4
Hello World from thread 3
Hello World from thread 2
Hello World from thread 1
Fin

Cela est très utile pour exécuter une action après la fin du système et vérifier quelque chose sur le système. Par exemple, afficher un message dans le cas d'un deadlock ou afficher des informations de debug sur les traitements effectués.

5 - Sémaphores

Nous allons maintenant voir comment utiliser un des concepts de base de la programmation concurrente : les sémaphores. Les sémaphores sont un concept très simple, mais très puissant de la programmation concurrente. Un sémaphore a une certaine valeur entière représentant le nombre de threads qu'il va laisser passer, appelons-la « s ». Un sémaphore a deux actions :

- P : met le thread en attente tant que s égale 0 puis décrémente s ;

- V : incrémente s.

Ces deux opérations sont atomiques. Nous utilisons les sémaphores pour protéger une section critique qui doit être effectuée de manière atomique. On peut également utiliser les sémaphores pour permettre à un certain nombre de threads d'exécuter parallèlement une certaine série d'instructions.

La déclaration et l'utilisation des sémaphores en JR est extrêmement simple. Voici comment déclarer un sémaphore avec une valeur initiale de 1 :

sem mutex = 1;

À la suite de cela, les opérations P et V sont extrêmement simples à utiliser :

P(mutex);
//Section critique
V(mutex);

Ce sont des méthodes disponibles de base. Comme dit précédemment, les sémaphores sont principalement utilisés pour rendre des sections atomiques. Imaginons un exemple très simple, mais qui montre bien le problème que les sémaphores permettent de résoudre :

private static int value = 0;

static process Calculator((int id = 0; id < 50; id++)){
for(int i = 0; i < 5; i++){
value = value + 2;
}
}

Comme ce code lance 50 threads qui ajoutent chacun 5 fois 2 à value, value devrait valoir 500 à la fin de l'exécution, n'est-ce pas ?

Mais rien ne permet de garantir ce résultat de cette manière. C'est le phénomène d'entrelacement qui est propre à la programmation concurrente. En effet, l'opération qui augmente de deux est formée de plusieurs opérations :

1. Lecture de la valeur de value ;

2. Ajout de 2 à cette valeur ;

3. Attribue la nouvelle valeur à value.

Un thread peut donc être mis en attente juste après 1 et faire l'incrémentation alors que d'autres threads l'ont déjà fait, mais il possède encore l'ancienne valeur de value lorsqu'il fait le +2 et écrit donc une valeur « erronée » dans value. Pour vous le prouver, exécuter le code suivant plusieurs fois :

import edu.ucdavis.jr.JR;

public class SemaphoreProcess {
private static int value = 0;

static process Calculator((int id = 0; id < 50; id++)){
for(int i = 0; i < 5; i++){
value = value + 2;
}
}

public static void main(String[] args){
try {
JR.registerQuiescenceAction(fin);
} catch (edu.ucdavis.jr.QuiescenceRegistrationException e){
e.printStackTrace();
}
}

public static op void fin(){
System.out.println(value);
}
}

Sur ma machine, j'obtiens les résultats suivants :

498
500
500
500
496

Et si nous utilisons des valeurs plus grandes, c'est encore pire. Par exemple avec 100 threads et 100 itérations :

20000
19560
19912
19758
20000

Ce problème est très facilement résoluble avec les sémaphores :

private static sem mutex = 1;
private static int value = 0;

static process Calculator((int id = 0; id < 50; id++)){
for(int i = 0; i < 5; i++){
P(mutex);
value = value + 2;
V(mutex);
}
}

Avec cela, nous avons la garantie qu'un seul thread fasse l'incrémentation en même temps et elle devient de ce fait atomique. Ainsi toutes les exécutions finiront avec une valeur de 500. Bien entendu, cela impacte grandement les performances étant donné qu'au lieu d'avoir x threads qui font constamment des opérations, nous limitons cette fois le nombre de threads pouvant exécuter une série d'instructions. L'exemple avec 100 threads et 100 itérations devient très lent. Nous pourrions améliorer les performances en utilisant le mutex autour de la boucle for, mais cela réduirait encore l'intérêt des threads. Il faut donc utiliser à bon escient les techniques de synchronisation de threads.

6 - Conclusion

Voilà, nous avons maintenant couvert les concepts de base du langage de programmation JR. Comme vous avez pu le voir, ce langage de programmation permet de simplifier grandement le traitement des concepts de programmation concurrente.

J'espère que ce tutoriel vous a fait découvrir le langage JR et vous a donné envie d'en savoir plus.

N'hésitez pas à donner votre avis sur le forum : Commentez

6-1 - Remerciements

Merci à Wachter pour sa correction.