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 25 est là : https://igm.univ-mlv.fr/~juge/javadoc-25/.

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

MailBox

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

Le but de cet exercice est de modéliser une classe thread-safe MailBox qui permet de stocker des messages. La classe peut stocker un nombre limité de messages fixé à la construction de l'objet. La classe possède deux méthodes : store et retrieve.

La méthode store(String message, int priority) permet de stocker un message dans la boîte aux lettres. Si la boîte aux lettres est pleine, le thread appelant la méthode store est mis en attente jusqu'à ce qu'une place soit libre. Dans le cas où plusieurs threads sont en attente, quand un espace se libère, c'est le thread ayant la priorité la plus haute qui est choisi pour stocker son message. S'il y a plusieurs threads avec la même priorité, c'est le thread qui a été mis en attente le plus longtemps qui est choisi.

La méthode String retrieve() permet de récupérer le message le plus ancien de la boîte aux lettres. Si la boîte aux lettres est vide, le thread appelant la méthode retrieve est mis en attente jusqu'à ce qu'un message soit disponible. La méthode renvoie le message récupéré.

Donner une implémentation thread-safe dans une classe MailBox qui utilise des ReentrantLock.

Vous pouvez tester votre classe avec le code suivant :

    public static void sleepInterruptibly(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
        var mailbox = new MailBox(1);
        for (var i = 1; i <= 10; i++) {
            var id = i;
            var message = "msg " + id;
            Thread.ofPlatform().name("Store-thread-" + id).start(() -> {
                try {
                    sleepInterruptibly(id * 1000);
                    System.out.println(
                            Thread.currentThread().getName() + " waiting to store " + message + " with priority " + id);
                    mailbox.store(message, id);
                    System.out.println(Thread.currentThread().getName() + " store " + message + " with priority " + id);
                } catch (InterruptedException e) {
                    throw new AssertionError(e);
                }
            });
        }
        Thread.sleep(15_000);
        for (var i = 1; i <= 10; i++) {
            Thread.sleep(1_000);
            System.out.println(Thread.currentThread().getName() + " remove " + mailbox.retrieve());
        }

Ce code doit donner l'affichage suivant :

    Store-thread-1 waiting to store msg 1 with priority 1
    Store-thread-1 store msg 1 with priority 1
    Store-thread-2 waiting to store msg 2 with priority 2
    Store-thread-3 waiting to store msg 3 with priority 3
    Store-thread-4 waiting to store msg 4 with priority 4
    Store-thread-5 waiting to store msg 5 with priority 5
    Store-thread-6 waiting to store msg 6 with priority 6
    Store-thread-7 waiting to store msg 7 with priority 7
    Store-thread-8 waiting to store msg 8 with priority 8
    Store-thread-9 waiting to store msg 9 with priority 9
    Store-thread-10 waiting to store msg 10 with priority 10
    main remove msg 1
    Store-thread-10 store msg 10 with priority 10
    main remove msg 10
    Store-thread-9 store msg 9 with priority 9
    main remove msg 9
    Store-thread-8 store msg 8 with priority 8
    main remove msg 8
    Store-thread-7 store msg 7 with priority 7
    main remove msg 7
    Store-thread-6 store msg 6 with priority 6
    main remove msg 6
    Store-thread-5 store msg 5 with priority 5
    main remove msg 5
    Store-thread-4 store msg 4 with priority 4
    main remove msg 4
    Store-thread-3 store msg 3 with priority 3
    main remove msg 3
    Store-thread-2 store msg 2 with priority 2
    main remove msg 2

On veut ajouter une méthode Set<Thread> waitingToRetrieve() qui renvoie l'ensemble des threads en attente de prendre un message. Ajouter cette méthode à votre classe MailBox.


On veut maintenant spécifier le comportement de nos méthodes en cas d'interruption. Si la méthode retrieve est interrompue, elle lève une InterruptedException et tous les threads en attente de prendre un message sont interrompus. Si la méthode store est interrompue, elle lève une InterruptedException.
Copiez votre classe MailBox dans une classe MailBoxInterrupt et modifiez-la pour obtenir le comportement demandé.

Vous pouvez tester votre classe avec le code suivant :

        var mailbox = new MailBoxInterrupt(1);
        var threads = new Thread[10];
        for(var i = 0; i < 10; i++) {
            var id = i;
            threads[id] = Thread.ofPlatform().name("Retrieve-thtread-" + id).start(() -> {
                try {
                    mailbox.retrieve();
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + " interrupted");
                }
            });
        }
        Thread.sleep(5_000);
        threads[0].interrupt();
        System.out.println("Interrupting" + threads[0].getName());

Ce code doit donner un affichage similaire à l'affichage suivant :

    InterruptingRetrieve-thtread-0
    Retrieve-thtread-6 interrupted
    Retrieve-thtread-9 interrupted
    Retrieve-thtread-1 interrupted
    Retrieve-thtread-4 interrupted
    Retrieve-thtread-0 interrupted
    Retrieve-thtread-5 interrupted
    Retrieve-thtread-2 interrupted
    Retrieve-thtread-8 interrupted
    Retrieve-thtread-7 interrupted
    Retrieve-thtread-3 interrupted    

On veut maintenant rajouter une méthode void close() qui attend qu'il n'y ait plus aucun thread en attente de stocker un message pour fermer la boîte aux lettres. Les threads en attente de récupérer un message lèvent une AsynchronousCloseException. Les threads appelant l'une des méthodes après la fermeture lèvent une IllegalStateException.
Copiez votre classe MailBoxInterrupt dans une classe MailBoxClosable et modifiez-la pour obtenir le comportement demandé.

Factorio

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

Dans cet exercice, on vous fournit une petite API factice Factorio.java qui simule un système complet d'extraction de minerai et production de barres métalliques et d'engrenages.

L'API Factorio définit des records pour le minerai de fer et de cuivre.

    public record IronOre(long sampleId) implements Ore {}

    public record CopperOre(long sampleId) implements Ore {}

L'API permet d'extraire du minerai de fer et de cuivre avec les méthodes mineIron et mineCopper.

    static IronOre mineIron() throws InterruptedException { ... }

    static CopperOre mineCopper() throws InterruptedException { ... }

L'API permet de fondre le minerai de fer et de cuivre avec les méthodes smeltIronOre et smeltCopperOre pour produire des barres de fer et de cuivre.

    static IronBar smeltIronOre(IronOre ore) throws InterruptedException { ... }

    static CopperBar smeltCopperOre(CopperOre ore) throws InterruptedException { ... }

Les barres de fer et de cuivre sont représentées par les records IronBar et CopperBar et ont une taille comprise entre 1 et 10 inclus. On ne contrôle pas la taille des barres de fer et de cuivre qui vont être produites par les méthode smeltIronOre ou smeltCopperOre.

    public record IronBar(long id, int size) { ... }

    public record CopperBar(long id, int size) { ... }

Enfin, l'API permet de produire des engrenages avec la méthode forgeCog. La méthode prend en paramètre la taille size de l'engrenage à produire, une barre de fer et une barre de cuivre de taille size. Elle renvoie l'engrenage produit.

    static Cog forgeCog(int size, IronBar ironBar, CopperBar copperBar) throws InterruptedException { ... }

Le morceau de code ci-dessous montre comment on peut produire un engrenage de taille 2. Il mine et fond un minerai de fer jusqu'à obtenir une barre de fer de taille 2. Il fait de même pour le minerai de cuivre. Il produit ensuite l'engrenage de taille 2.

    var size = 2;
    IronBar ironBar = null;
    CopperBar copperBar = null;
    for (;;) {
        var ironOre = mineIron();
        ironBar = smeltIronOre(ironOre);
        if (ironBar.size() == size) {
            break;
        }
    }
    for (;;) {
        var copperOre = mineCopper();
        copperBar = smeltCopperOre(copperOre);
        if (copperBar.size() == size) {
            break;
        }
    }
    var cog = forgeCog(size, ironBar, copperBar);
    System.out.println(cog);    

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 :

    CopperOreMiner mined CopperOre[sampleId=651742]
    CopperOreMiner mined CopperOre[sampleId=535289]
    IronOreMiner mined IronOre[sampleId=484406]
    CopperBarsSmelter-0 smelted CopperOre[sampleId=651742] into CopperBar[id=739991, size=3]
    CopperBarsSmelter-1 smelted CopperOre[sampleId=535289] into CopperBar[id=44291, size=10]
    CopperOreMiner mined CopperOre[sampleId=515074]
    IronOreMiner mined IronOre[sampleId=184642]
    CopperOreMiner mined CopperOre[sampleId=826321]
    IronOreMiner mined IronOre[sampleId=376430]
    IronBarsSmelter-0 smelted IronOre[sampleId=484406] into IronBar[id=309366, size=7]
    CopperOreMiner mined CopperOre[sampleId=842316]
    IronOreMiner mined IronOre[sampleId=456594]
    IronOreMiner mined IronOre[sampleId=548817]
    CopperBarsSmelter-0 smelted CopperOre[sampleId=515074] into CopperBar[id=981119, size=5]
    IronBarsSmelter-1 smelted IronOre[sampleId=184642] into IronBar[id=935518, size=3]
    CopperBarsSmelter-1 smelted CopperOre[sampleId=826321] into CopperBar[id=216759, size=2]
    IronOreMiner mined IronOre[sampleId=388114]
    IronBarsSmelter-1 smelted IronOre[sampleId=456594] into IronBar[id=406543, size=5]
    CopperOreMiner mined CopperOre[sampleId=55460]
    IronBarsSmelter-0 smelted IronOre[sampleId=376430] into IronBar[id=63589, size=1]
    CopperBarsSmelter-1 smelted CopperOre[sampleId=55460] into CopperBar[id=941872, size=1]
    IronOreMiner mined IronOre[sampleId=495540]
    CopperBarsSmelter-0 smelted CopperOre[sampleId=842316] into CopperBar[id=91835, size=7]
    IronOreMiner mined IronOre[sampleId=829895]
    IronBarsSmelter-0 smelted IronOre[sampleId=388114] into IronBar[id=187977, size=5]
    CopperOreMiner mined CopperOre[sampleId=602924]
    CogMakerSize-1 forged IronBar[id=63589, size=1] and CopperBar[id=941872, size=1] into Cog[id=41039, size=1]
    IronBarsSmelter-1 smelted IronOre[sampleId=548817] into IronBar[id=579763, size=8]
    CopperOreMiner mined CopperOre[sampleId=132010]
    CogMakerSize-3 forged IronBar[id=935518, size=3] and CopperBar[id=739991, size=3] into Cog[id=808596, size=3]
    IronOreMiner mined IronOre[sampleId=809447]
    IronBarsSmelter-1 smelted IronOre[sampleId=829895] into IronBar[id=46323, size=6]
    IronOreMiner mined IronOre[sampleId=328034]
    IronBarsSmelter-0 smelted IronOre[sampleId=495540] into IronBar[id=527744, size=1]
    IronOreMiner mined IronOre[sampleId=756773]    

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


On veut maintenant que lorsque la somme des tailles des engrenages produits atteint 500, le système s'arrête. Écrire le main correspondant dans une classe ApplicationStopping.


On suppose maintenant que les méthodes mineIron et mineCopper sont remplacées par une unique méthode Ore mine() qui renvoie au hasard un minerai de fer ou de cuivre avec une chance sur 2 chacun.

Note: L'interface Ore est sealed, on peut donc savoir quel type de minerai a été produit en utilisant un switch.

public sealed interface Ore permits IronOre, CopperOre {}    

On souhaiterait adapter de la classe Application de la première question en modifiant le code des threads qui utilisent mineIron et mineCopper pour qu'ils utilisent tous les deux la méthode mine. Expliquez le problème posé par cette modification.

DeftNote

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

Le but de l'exercice est d'écrire une classe thread-safe DeftNote qui représente un carnet qui permet à un enseignant de noter ses étudiants les plus remarquables.

Dans une premier temps, on veut écrire un classe thread-safe DeftNote qui possède une méthode boolean grade(String student, int grade). Cette méthode permet de noter dans le carnet un étudiant avec une note entre 0 et 20. Elle renvoie true si la note est la meilleure enregistrée par l'enseignant. Elle renvoie false sinon.

Donner une implémentation thread-safe dans une classe DeftNote qui n'utilise ni section critique, ni verrou et en utilisant les classes Atomic*.

    var deftNote = new DeftNote();
    System.out.println(deftNote.grade("Alice", 10)); // true
    System.out.println(deftNote.grade("Bob", 8)); // false
    System.out.println(deftNote.grade("Pablo", 17)); // true
    System.out.println(deftNote.grade("Pablo", 11)); // false


On veut maintenant écrire une deuxième version du carnet, DeftNoteExtra, qui possède une méthode OptionalDouble grade(String student, int grade). Si le même étudiant est noté deux fois d'affilée dans le carnet avec une note supérieure ou égale à 15 alors la méthode renvoie un OptionalDouble contenant la moyenne des deux notes et sinon elle renvoie un OptionalDouble vide.

Donner une implémentation thread-safe de la classe DeftNoteExtra qui n'utilise ni section critique, ni verrou et en utilisant les VarHandle.

    var deftNote = new DeftNoteExtra();
    System.out.println(deftNote.grade("Alice", 10)); // OptionalDouble.empty
    System.out.println(deftNote.grade("Alice", 17)); // OptionalDouble.empty
    System.out.println(deftNote.grade("Bob", 16)); // OptionalDouble.empty
    System.out.println(deftNote.grade("Alice", 16)); // OptionalDouble.empty
    System.out.println(deftNote.grade("Alice", 18)); // OptionalDouble[17.0]
    System.out.println(deftNote.grade("Bob", 16)); // OptionalDouble.empty
    System.out.println(deftNote.grade("Bob", 16)); // OptionalDouble[16.0]