• Aucun résultat trouvé

Exécution parallèle et concurrente

Pour assurer une réactivité maximale, l’exécution des règles est parallélisée. Un cer-tain nombre de mécanismes permet cette exécution parallèle sans conflit d’accès aux données, tout en garantissant des performances optimales.

5.2.1 Exécution parallèle des règles

Chaque règle fait l’objet d’une classe Java conforme à l’interface de la figure 5.3. Une instance de cette classe correspond à un exécuteur de règle.

Algorithme 5.2 – Distribution des triples inférés aux différents buffers abonnés

Données : abonnes, abonnesUniversels, triplesADistribuer

1 pour buffer ∈ abonnesUniversels faire

2 buffer.envoyer(triplesADistribuer)

3 fin

4 pour triple ∈ triplesADistribuer faire

5 pour buffer ∈ abonnes.get(triple.predicat) faire

6 buffer.envoyer(triple)

7 fin

8 fin

1 public interfaceRuleExecutor{

2 publicCollection<Triple>execute(TripleStore ts,TripleStore newTriples);

3 }

Figure 5.3 – Interface Java des règles d’inférence utilisables dans l’architecture proposée

Lorsqu’un buffer est plein ou a atteint le timeout, il notifie un exécuteur de règle en charge d’appliquer la règle d’inférence. L’exécuteur de règle commence par récu-pérer les triples contenus dans le buffer. Comme nous l’avons vu dans la sous-section précédente, le nombre de triples récupérés dépend de la raison de l’instanciation de l’exécuteur : soit le buffer est plein, soit il a atteint le timeout. Si le buffer est plein, le nombre de triples récupérés correspond à la limite fixée, ou taille du buffer. Si le timeout a été atteint, le nombre de triples récupérés est celui du compteur au moment du timeout. Afin de simplifier la recherche d’éléments dans l’ensemble des triples ainsi récupérés, ils sont regroupés dans une structure identique à celle du triplestore, à savoir un partitionnement vertical.

Une fois les triples récupérés depuis le buffer, il reste à trouver dans ces triples les ensembles de triples satisfaisant les prémisses de la règle afin d’en déduire les conclusions. Un accès au triplestore lui permet de compléter des correspondances. L’algorithme 5.3 illustre l’implémentation de la règle cax-sco.

Chaque exécuteur de règle est implémenté comme un processus léger (ou thread). Toutes les instances d’exécuteur de règle sont envoyées à un gestionnaire de processus légers (ou threadpool). Ce dernier exécute les processus qui lui sont soumis en fonction des ressources système disponibles. Si celles-ci deviennent insuffisantes, les nouveaux processus sont mis en file d’attente pour être exécutés lorsque les ressources nécessaires

Algorithme 5.3 – Implémentation de la règle d’inférence cax-sco

Données : triplesbuffer, triplestore Résultat : triplesinferes

1 triplesinferes← ∅

2 triplesSubclassof←triplestore.predicat(subclassof)

3 si triplesSubclassof, ∅ alors

4 triplesType=triplesbuffer.predicate(type)

5 pour tripleType ∈ triplesType faire

6 pour tripleSCO ∈ triplesSubclassof.sujet(tripleType.objet) faire

7 triplesinferes←(tripleType.sujet,type,tripleSCO.objet) 8 fin 9 fin 10 fin 11 triplesSubclassof←triplesbuffer.predicat(subclassof) 12 si triplesSubclassof, ∅ alors 13 triplesType=triplestore.predicate(type)

14 pour tripleType ∈ triplesType faire

15 pour tripleSCO ∈ triplesSubclassof.sujet(tripleType.objet) faire

16 triplesinferes←(tripleType.sujet,type,tripleSCO.objet)

17 fin

18 fin

19 fin

sont libérées. Grâce à ce principe, l’exécution de chaque exécuteur de règle est garantie, tout en évitant une surcharge des ressources disponibles.

L’implémentation des exécuteurs de règle suit l’interface de la figure 5.3. Toute règle dont l’implémentation est conforme à cette interface peut être utilisée dans Slider, y compris des règles qui ne sont pas définies par le W3C. Pour faciliter la recherche des prémisses dans les triples du buffer et maximiser la réutilisation du code existant, ces derniers sont stockés dans une structure identique au triplestore.

5.2.2 Gestion de la concurrence

La parallélisation d’un programme nécessite un certain nombre de garanties. En cas d’accès simultané à une ressource par plusieurs processus, nous devons nous assurer que les données restent consistantes, et que les processus ne se retrouvent jamais dans des situations de blocage mutuel. Pour cela, chaque objet accessible par différents processus dispose d’un système de verrous. Deux niveaux de verrous sont utilisés : un pour l’écriture et un pour la lecture. Le verrou en écriture empêche tout processus d’accéder

à la ressource verrouillée, alors que le verrou en lecture autorise les autres processus à lire les données de la ressource, mais interdit toute modification.

Le triplestore, le dictionnaire et les buffers, dont les données sont accessibles par plusieurs processus du raisonneur, disposent donc de verrous en lecture et en écri-ture. L’objet Java ReentrantReadWriteLock implémente un système de verrous pour la lecture et l’écriture. Les instructionsrwlock.readLock().lock()etrwlock.writeLock

().lock() permettent respectivement de verrouiller le verrou en lecture et en écriture. Les instructions similaires rwlock.writeLock().unlock()et rwlock.readLock().unlock

()permettent de déverrouiller le verrou. Les objets sont verrouillés de manière globale. Nous avons implémenté un triplestore gérant différents niveaux de verrous. Le verrou global est utilisé pour l’accès à la liste des prédicats. Un verrou par prédicat est ensuite utilisé pour l’accès aux sujets et aux objets associés. Les performances de cette version du triplestore sont cependant moins intéressantes. Cela peut s’expliquer par le surcoût des multiples verrouillages et déverrouillages que son utilisation entraîne. Nous avons donc conservé le triplestore utilisant un verrou global.

L’algorithme 5.4 illustre l’utilisation d’un verrou pour l’ajout d’un triple dans un

buffer. L’accès en écriture sur le buffer est verrouillé au début de l’algorithme et n’est

relâché qu’après tous les accès aux données du buffer, à la fin de l’algorithme. Cela permet de garantir la consistance des données pendant cette opération.

Algorithme 5.4 – Ajout d’un triple dans un buffer avec l’utilisation d’un verrou

Données : buffer, triple, moduleregle

1 rwlock.writeLock().lock() 2 buffer.file.ajout(triple) 3 s buffer.compteur++ 4 si buffer.compteur>=buffer.limite alors 5 moduleregle.notifie() 6 buffer.compteur← 0 7 fin 8 rwlock.writeLock().unlock()

5.2.3 Initialisation et fin de l’inférence

Pour être générique, Slider ne demande aucune intervention supplémentaire lors de son utilisation, hormis le choix du fragment de règles d’inférence. Seules les règles compo-sant ce fragment sont utilisées. Durant la phase d’initialisation, le graphe de dépendance des règles (c.f. section 4.5.1) est calculé et utilisé afin de définir vers quels buffers chaque

distributeur enverra les triples qu’il reçoit. Pour que ce mécanisme fonctionne, il est

prédicats utilisés par la règle correspondante, et ceux qu’elle est susceptible de générer. Grâce à ces informations, le raisonneur peut déterminer quelle règle peut utiliser les triples inférés par une autre règle. Si la liste des prédicats pouvant être utilisés par la règle est laissée vide, le système considérera cette règle comme universelle.

L’algorithme 5.5 est utilisé à l’initialisation du raisonneur pour connecter les

dis-tributeurs aux buffers. Il donne à chaque distributeur la liste des buffers auxquels

envoyer les triples inférés en fonction du prédicat du triple. UneMultiMap est une va-riante deHashMapfaisant correspondre à une clé plusieurs valeurs. Nous utilisons cette structure pour faire correspondre à un prédicat un ensemble de buffers à qui envoyer les triples avec ce prédicat. UneHashMapcontient les buffers universels, auxquels tous les triples doivent être envoyés.

Algorithme 5.5 – Création des liens entre les règles à l’initialisation du raisonneur

Données : M dR les modules de règles

1 pour (M dR1, M dR2) ∈ M dR × M dR faire

2 premisses1← P remisses(M dR1.executeurDeRegle)

3 premisses2← P remisses(M dR1.executeurDeRegle)

4 conclusions1← Conclusions(M dR1.executeurDeRegle)

5 conclusions2← Conclusions(M dR2.executeurDeRegle)

6 si conclusions1== ∅ alors

7 M dR1.distributeur.abonnerU niversel(M dR2.buf f er)

8 fin

9 si conclusions1∈1premisses2 alors

10 M dR1.distributeur.abonner(M dR2.buf f er)

11 fin

12 si conclusions2== ∅ alors

13 M dR2.distributeur.abonnerU niversel(M dR1.buf f er)

14 fin

15 si conclusions2∈1premisses1 alors

16 M dR2.distributeur.abonner(M dR1.buf f er)

17 fin

18 fin

Pour ajouter une règle pendant l’exécution du raisonneur, un algorithme similaire est utilisé. Au lieu de boucler sur tous les couples de modules de règles (ligne 1), il ne boucle que sur ceux contenant la nouvelle règle. L’algorithme passe donc d’une complexité O(n2) où n est le nombre de règles à O(n). Le coût à l’exécution est donc réduit, ce qui permet d’ajouter efficacement une nouvelle règle au système pendant son exécution.

Comme nous l’avons décrit dans la sous-section 4.4.6, la détection de la fin de l’infé-rence n’est pas triviale. Pour cela, nous effectuons une vérification portant sur le nombre d’exécuteurs de règle en cours d’exécution et de buffers vides. Cette vérification est faite à la fin de l’exécution de chaque exécuteur de règles. L’algorithme 5.6

illustre l’implémentation de cette méthode. L’objet phaser est un entier immuable (un

AtomicInteger) partagé par tous les processus légers du raisonneur. Il est utilisé pour notifier le système de la fin de l’exécution d’une règle, et donc effectuer le test de détec-tion de la fin de l’inférence. Sa seconde utilité est de connaître le nombre d’exécuteurs

de règle en cours d’exécution. Chaque exécuteur de règle incrémente la valeur du phaser à son instanciation, et la décrémente à la fin de son exécution. Cela permet

de déterminer facilement et efficacement le nombre d’exécuteurs de règle en cours d’exécution.

Algorithme 5.6 – Détection de la fin du raisonnement

1 buffersNonVides = nombreBuffersNonVides() tant que buffersNonVides>0

faire

2 enCours = phaser

3 tant que enCours>0 faire

4 phaser.enAttente()// Notifié par les exécuteurs de règle

5 fin

6 buffersNonVides = nombreBuffersNonVides()

7 fin