Examen 2h sur machine


L'examen est composé de 3 exercices indépendants qui peuvent être traités dans l'ordre que vous voulez.

Rappel : si ce n'est pas déjà fait, vous devez configurer le workspace d'Eclipse (File > Switch WorkSpace) pour qu'il corresponde au répertoire EXAM présent dans le home de votre session de TP noté.

Vérifier que tous vos fichiers sont bien dans le répertoire EXAM. Tout ce qui n'est pas dans ce répertoire est perdu quand vous vous déconnectez.

La javadoc 23 est là : https://igm.univ-mlv.fr/~juge/javadoc-23/.

Vous avez le droit de consulter les transparents du cours pendant l'examen.

Piège de cristal

Les classes de cet exercice doivent être dans package fr.uge.concurrence.exo1.

On veut créer une classe thread-safe Trap. Dans un premier temps, cette classe possédera une unique méthode void propose(String key). Le comportement de cette méthode est le suivant : lorsqu'un thread appelle cette méthode avec une valeur key, il reste bloqué jusqu'à ce qu'un autre thread appelle la méthode propose avec la même valeur key. Ce thread est à son tour bloqué (et le précédent est débloqué) jusqu'à ce qu'un autre thread appelle avec la même valeur et ainsi de suite.

Écrire la classe Trap.

Vous pouvez tester avec le main suivant :

public static void main(String[] args) throws InterruptedException {
    var trap = new Trap();
    var nbThreads = 20;
    LongStream.range(0, nbThreads).forEach(id -> {
        Thread.ofPlatform().start(() -> {
            try {
                Thread.sleep(id * 1_000);
                var value = "" + Math.abs(ThreadLocalRandom.current().nextInt(0, 3));
                System.out.println("Thread " + id + " proposes " + value + " and is blocked");
                trap.propose(value);
                System.out.println("Thread " + id + " is unblocked");
            } catch (InterruptedException e) {
                throw new AssertionError();
            }
        });
    });
}

Vous devriez avoir un affichage similaire à celui ci-dessous. Remarquez que le programme ne s'arrête pas.

Thread 0 proposes 0 and is blocked
Thread 1 proposes 0 and is blocked
Thread 0 is unblocked
Thread 2 proposes 2 and is blocked
Thread 3 proposes 0 and is blocked
Thread 1 is unblocked
Thread 4 proposes 0 and is blocked
Thread 3 is unblocked
Thread 5 proposes 1 and is blocked
Thread 6 proposes 0 and is blocked
Thread 4 is unblocked
Thread 7 proposes 1 and is blocked
Thread 5 is unblocked
Thread 8 proposes 2 and is blocked
Thread 2 is unblocked
Thread 9 proposes 2 and is blocked
Thread 8 is unblocked
Thread 10 proposes 0 and is blocked
Thread 6 is unblocked
Thread 11 proposes 2 and is blocked
Thread 9 is unblocked
Thread 12 proposes 1 and is blocked
Thread 7 is unblocked
Thread 13 proposes 2 and is blocked
Thread 11 is unblocked
Thread 14 proposes 1 and is blocked
Thread 12 is unblocked
Thread 15 proposes 0 and is blocked
Thread 10 is unblocked
Thread 16 proposes 0 and is blocked
Thread 15 is unblocked
Thread 17 proposes 2 and is blocked
Thread 13 is unblocked
Thread 18 proposes 0 and is blocked
Thread 16 is unblocked
Thread 19 proposes 2 and is blocked
Thread 17 is unblocked


Afin de pouvoir arrêter le programme, on veut ajouter une méthode int releaseAll() qui permet de débloquer tous les threads actuellement bloqués dans le Trap. Elle renvoie le nombre de threads ainsi débloqués. À partir du moment où elle a été appelée, la méthode propose devient sans effet. Écrire la méthode releaseAll. Pour tester, vous pouvez modifier votre programme pour que le main appelle releaseAll après 20 secondes.

Copiez la classe Trap que vous avez obtenue pour l'instant dans une classe TrapV1 et continuez à modifier la classe Trap. On veut ajouter une autre méthode permettant de libérer des threads, mais pas nécessairement tous les threads. La méthode Set<Thread> release(int nb) attend qu'il y ait au moins nb thread bloqués dans le Trap et elle en débloque exactement nb. Les threads débloqués sont choisis arbitrairement et sont renvoyés par la méthode. Si la méthode releaseAll est appelée, les threads bloqués en attente dans la méthode release lèvent une AsynchronousCloseException. Écrire la méthode release.

Modifier votre programme pour que le main appelle release(2) toutes les 500 millisecondes et affiche les threads libérés. Faire en sorte que releaseAll soit appelé au bout de 20 secondes par un autre thread et que le programme s'arrête. Vous devriez avoir un affichage similaire à celui ci-dessous (attention, il reste encore une question ensuite).

Thread 0 proposes 0 and is blocked
try to release 2 threads
Thread 1 proposes 0 and is blocked
Thread 0 is unblocked
Thread 2 proposes 2 and is blocked
Thread 1 is unblocked
Thread 2 is unblocked
[Thread[#22,Thread-2,5,main], Thread[#21,Thread-1,5,main]]
try to release 2 threads
Thread 3 proposes 1 and is blocked
Thread 4 proposes 1 and is blocked
Thread 3 is unblocked
Thread 5 proposes 0 and is blocked
Thread 5 is unblocked
Thread 4 is unblocked
[Thread[#24,Thread-4,5,main], Thread[#25,Thread-5,5,main]]
try to release 2 threads
Thread 6 proposes 0 and is blocked
Thread 7 proposes 1 and is blocked
Thread 7 is unblocked
Thread 6 is unblocked
[Thread[#27,Thread-7,5,main], Thread[#26,Thread-6,5,main]]
try to release 2 threads
Thread 8 proposes 1 and is blocked
Thread 9 proposes 0 and is blocked
Thread 8 is unblocked
Thread 9 is unblocked
[Thread[#29,Thread-9,5,main], Thread[#28,Thread-8,5,main]]
try to release 2 threads
Thread 10 proposes 1 and is blocked
Thread 11 proposes 2 and is blocked
[Thread[#30,Thread-10,5,main], Thread[#31,Thread-11,5,main]]
Thread 10 is unblocked
Thread 11 is unblocked
try to release 2 threads
Thread 12 proposes 1 and is blocked
Thread 13 proposes 1 and is blocked
Thread 12 is unblocked
Thread 14 proposes 1 and is blocked
Thread 13 is unblocked
Thread 15 proposes 1 and is blocked
Thread 14 is unblocked
Thread 16 proposes 2 and is blocked
[Thread[#35,Thread-15,5,main], Thread[#36,Thread-16,5,main]]
Thread 16 is unblocked
Thread 15 is unblocked
try to release 2 threads
Thread 17 proposes 0 and is blocked
Thread 18 proposes 0 and is blocked
Thread 17 is unblocked
Thread 19 proposes 0 and is blocked
Thread 18 is unblocked
Thread 19 is unblocked
released 1 threads

Copiez la classe Trap que vous avez obtenue pour l'instant dans une classe TrapV2 et continuez à modifier la classe Trap. Modifier la classe Trap de telle façon que si un thread est interrompu alors qu'il est bloqué dans la méthode propose, il est débloqué et lève une InterruptedException et tous les threads également bloqués dans propose sont aussi interrompus.

LLMSimulator

Les classes de cet exercice doivent être dans package fr.uge.concurrence.exo2.

Dans cet exercice, on vous fournit une petite API factice LLMSimulator.java qui simule un système complet permettant de récupérer des questions pour des LLMs (ChatGPT, ...) et de faire générer la réponse par le LLM. Le résumé ci-dessous est suffisant pour faire cet exercice et il n'est pas nécessaire de lire le code de LLMSimulator.

L'API LLMSimulator fournit les éléments suivants :

  1. Une enum LLM représentant les LLM disponibles :

    public enum LLM { CHATGPT, BARD, CLAUDE, LLAMA }
        
  2. Des records représentant respectivement une question pour un LLM et un réponse du LLM.
    public record Question(String content, LLM llm);
    public record Answer(String question, String answer, LLM llm);
        
  3. Les méthodes principales sont :
    • Question retrieveQuestion() qui simule la récupération d'une question sur internet,
    • Answer createAnswer(Question question) qui génère la réponse à la question en utilisant le LLM donnée dans la question,
    • shouldCensor(Answer answer) qui vérifie si une réponse doit être censurée.

public static void main(String[] args) throws InterruptedException {
    LLMSimulator.Question question = LLMSimulator.retrieveQuestion();
    System.out.println("Question: " + question);

    LLMSimulator.Answer answer = LLMSimulator.createAnswer(question);
    System.out.println("Answer: " + answer);

    if (LLMSimulator.shouldCensor(answer)) {
        System.out.println("Answer is censored.");
    } else {
        System.out.println("Answer is not censored.");
    }
}

Il produit un affichage du type suivant :

Question: Question[content=Is there a debugger that understands my sarcasm?, llm=LLAMA]
Answer: Answer[question=Is there a debugger that understands my sarcasm?, answer=The ancient wisdom of troubleshooting suggests reading the guide., llm=LLAMA]
Answer is not censored.

Dans cet exercice, vous devez utiliser le pattern producteur-consommateur et toute autre forme de synchronisation sera considérée comme hors-sujet.

Dans le main d'une classe Application, vous de devez faire communiquer les threads suivants :

Tous les threads doivent afficher leurs actions de manière à obtenir un affichage semblable à celui donné ci-dessous :

AnswerTester-2 passed censorship: Answer[question=How do I explain to my manager that 'optimization' isn't just a magic button?, answer=The solution is probably written in the stars—or the manual., llm=LLAMA]
QuestionRetriever-0 retrieved: Question[content=Why does my code work only after I reboot the universe?, llm=CLAUDE]
QuestionRetriever-1 retrieved: Question[content=Why does my code run perfectly on Stack Overflow but fail on my machine?, llm=CLAUDE]
QuestionRetriever-2 retrieved: Question[content=How do I stop my regex from solving world hunger before it solves my problem?, llm=LLAMA]
QuestionRetriever-3 retrieved: Question[content=Why does my code run perfectly on Stack Overflow but fail on my machine?, llm=CHATGPT]
QuestionRetriever-4 retrieved: Question[content=How do I convince my code to respect me as its creator?, llm=LLAMA]
AnswerTester-0 passed censorship: Answer[question=Can I declare a variable that automatically makes me a better programmer?, answer=The ancient wisdom of troubleshooting suggests reading the guide., llm=BARD]
AnswerProducer-CLAUDE created: Answer[question=Why does my loop work perfectly until it encounters the number 42?, answer=Have you considered befriending the documentation?, llm=CLAUDE]
QuestionRetriever-2 retrieved: Question[content=Why do semicolons disappear when I’m not looking?, llm=CLAUDE]
AnswerProducer-CHATGPT created: Answer[question=Why does my loop work perfectly until it encounters the number 42?, answer=Maybe the author of the docs was trying to tell us something., llm=CHATGPT]    

Écrire le main de la classe Application demandée.

On veut maintenant que tous les threads s'arrêtent dès que 10 réponses d'un même LLM ont été censurées. Il peut être nécessaire de rajouter un ou des threads. Écrire le main correspondant dans une classe ApplicationStopping.

Les classes de cet exercice doivent être dans package fr.uge.concurrence.exo3.

Moyenne et milieu lock-free

On veut à créer une classe AverageLockFree thread-safe qui va mémoriser la moyenne de tous les nombres qui lui ont été soumis. Cette classe possède une méthode propose qui prend en paramètre un entier et une méthode average qui permet d'obtenir la moyenne des valeurs proposées (un double). Par convention, quand aucun nombre n'a été soumis, la moyenne vaut 0.
Donner une implémentation thread-safe dans une classe AverageLockFree qui n'utilise ni section critique, ni verrou et en utilisant la classe VarHandle.

Remarque : il n'est pas nécessaire de stocker toutes les valeurs soumises et la moyenne après ajout d'un nombre ne peut pas être recalculée à partir de la moyenne précédente. Vous pouvez vérifier que votre code calcule les bonnes valeurs avec le code ci-dessous.

public static void main(String[] args) {
    var average = new AverageLockFree();
    System.out.println(average.average()); // 0.0
    average.propose(1);
    System.out.println(average.average()); // 1.0
    average.propose(2);
    System.out.println(average.average()); // 1.5
    average.propose(3);
    System.out.println(average.average()); // 2.0
}

On veut à créer une classe MiddleLockFreeList thread-safe à laquelle on va pouvoir proposer des chaînes de caractères grâce à une méthode propose(String value). La méthode String middle() de cette classe permettra de renvoyer la chaîne qui se trouve au milieu de la liste des chaînes proposées. Si par exemple, les chaînes proposées sont dans l'ordre "Fifi", "Riri" et "Loulou", la méthode renverra "Riri". Plus précisément, si n chaînes ont été proposées, elle renvoie la chaîne dont l'indice est n/2 (division entière), en supposant qu'on numérote les chaînes dans l'ordre où elles ont été proposées, en commençant à l'indice 0. La méthode renvoie null si aucune chaîne n'a été proposée.
Donner une implémentation thread-safe dans une classe MiddleLockFreeList qui n'utilise ni section critique, ni verrou et en utilisant les classes Atomic*. En interne, vous devez utiliser une List pour stocker les chaînes. La méthode middle doit avoir une complexité O(1) mais vous n'avez pas de contraintes pour propose. Vous pouvez utiliser le code de la classe MiddleLockFreeTest.java pour tester.

public static void main(String[] args) {
    var middle = new MiddleLockFreeList();
    System.out.println(middle.middle()); // null
    middle.propose("A");
    System.out.println(middle.middle()); // A
    middle.propose("B");
    System.out.println(middle.middle()); // B
    middle.propose("C");
    System.out.println(middle.middle()); // B
    middle.propose("D");
    System.out.println(middle.middle()); // C
 }

On veut améliorer l'efficacité de notre classe. Pour cela, on va créer une classe MiddleLockFreeArray qui va utiliser un seul tableau (AtomicReferenceArray) pour stocker les chaînes proposées. La taille du tableau sera donnée à la construction de la classe et si plus de chaînes sont proposées, la méthode propose lèvera une IllegalStateException. La méthode middle doit toujours avoir une complexité O(1). La méthode propose doit avoir la complexité la plus basse possible.
Donner une implémentation thread-safe dans une classe MiddleLockFreeArray qui n'utilise ni section critique, ni verrou et en utilisant les classes Atomic*. Vous pouvez tester avec le code commenté de la classe MiddleLockFreeTest.java.

public static void main(String[] args) {
    var middle = new MiddleLockFreeArray(4);
    System.out.println(middle.middle()); // null
    middle.propose("A");
    System.out.println(middle.middle()); // A
    middle.propose("B");
    System.out.println(middle.middle()); // B
    middle.propose("C");
    System.out.println(middle.middle()); // B
    middle.propose("D");
    System.out.println(middle.middle()); // C
    middle.propose("E");                 // Exception in thread "main" java.lang.IllegalStateException
}