Concurrence

Retours sur le TD 04


Gestion des exceptions

Pour l'instant, nous ne gérons pas les InteruptedException et rien dans votre code ne peut provoquer la levée de cette exception.

  • si c'est possible, on renvoie l'exception (avec un throws),
  • sinon (par exemple dans un Runnable), on attrape l'exception et on la relance à run time.
    Runnable run = () -> {
        try {
        	Thread.sleep(20_000);
        } catch (InterruptedException e) {
        	// this should not happen !
        	throw new AssertionError(e);
        }
    };
    

On ne doit jamais voir e.printStackTrace();

Exo 2

BoundedSafeQueue

BoundedSafeQueue

public class BoundedSafeQueue<V> {
	private final ArrayDeque<V> queue = new ArrayDeque<>();
	private final int capacity;

	...

	public void add(V value) throws InterruptedException {
		Objects.requireNonNull(value);
		synchronized (queue) {
			while (queue.size() >= capacity) { 
				queue.wait(); 
			}
			queue.add(value);
			queue.notify();
		}
	}

	public V take() throws InterruptedException {
		synchronized (queue) {
			while (queue.isEmpty()) { 
				queue.wait(); 
			}
			queue.notify();
			return queue.remove();
		}
	}	

Quel est le problème?

Quel est le problème?

Supposons que 2 threads nommés t1 et t2 font des add et qu'un 3e thread, le main, fait des take, en continu, sur une file de capacité 1.

  • Le thread t1 démarre. La file est vide, il ajoute et fait une notification perdue. Il essaie d'ajouter à nouveau. La file est pleine, il attend.
  • Le thread t2 démarre. La file est pleine, il attend.
  • Le thread main démarre. Il retire une valeur et notifie. Puis il essaie de retirer une autre valeur, mais la file est vide, donc il attend.

  • t1 reçoit la notification, ajoute sa valeur et envoie une notification...
  • t2 reçoit la notification, mais la file est pleine donc il continue à attendre... t1 essaie d'ajouter, il ne peut pas, alors il attend aussi...
  • ... et le thread main ne recevra plus jamais de notification et donc il va attendre à l'infini (ou croiser les doigts pour un spurious wake-up).

Solution

public class BoundedSafeQueue<V> {
	private final ArrayDeque<V> queue = new ArrayDeque<>();
	private final int capacity;

	...

	public void add(V value) throws InterruptedException {
		Objects.requireNonNull(value);
		synchronized (queue) {
			while (queue.size() >= capacity) { 
				queue.wait(); 
			}
			queue.add(value);
			queue.notifyAll();
		}
	}

	public V take() throws InterruptedException {
		synchronized (queue) {
			while (queue.isEmpty()) { 
				queue.wait(); 
			}
			queue.notifyAll();
			return queue.remove();
		}
	}	

En fait, un seul des deux suffit...

Exo Max Bloquant

Correction rapide

Exo 3

Vote

Dans cet exercice, on veut réaliser une classe Vote. Cette classe thread-safe permet d'enregistrer n votes pour des String. Quand les n votes ont été enregistrés, on renvoie à chaque votant la String qui a reçu le plus de votes (en cas d'égalité, on renvoie la plus petite dans l'ordre alphabétique ayant reçu le plus de votes). Le nombre n de votes attendus est pris en paramètre par le constructeur.

La classe Vote offre une seule méthode vote qui sert à proposer son vote et qui bloque jusqu'à ce que les n votes soient arrivés. Ensuite elle renvoie le gagnant. Si vote est appelée après que les n votes aient été reçus, la méthode renvoie simplement le gagnant.

Principaux problèmes constatés

On calcule le gagnant après que le vote soit fini (et que d'autres votants peuvent arriver) : si un votant arrive après la fin du vote, il peut modifier le résultat !

On recalcule le gagnant à chaque fois qu'une valeur est renvoyée.

On fait trop de notifications : si chaque thread qui sort d'un wait notifie tous les autres.

Rappel sur les HashMap

Pour mettre à jour une HashMap<K,V> efficacement (V non mutable) :

	map.merge(key, 1, Interger::sum); // ou map.merge(key, 1, (x,y) -> x + y);

équivaut à :

	map.compute(key,(k, v) -> {
		if (v == null) {
		  return 1;
		} else {
		  return v + 1;
		}
	});

Rappel sur les HashMap

Au pire, si vous n'y arrivez pas avec une lambda (V toujours non mutable):

	var oldValue = map.get(key); // fait office d'appel à map.contains(key)
	if (oldValue == null) { 									
		oldValue = initialValue;
	}	
	V updatedValue = f(oldValue);
	map.put(key, updatedValue);

Ou mieux :

	var oldValue = map.getOrDefault(key,0); 
	V updatedValue = f(oldValue);
	map.put(key, updatedValue);

Rappel sur les HashMap

Pour mettre à jour une HashMap<K,V> efficacement (V mutable):

HashMap<Long,Data> map = ...

var data = map.computeIfAbsent<(key, k -> new Data());
data.update(newValue);

Par exemple :

HashMap<String,ArrayList<Integer>> map = ...

var data = map.computeIfAbsent(key, k -> new ArrayList<>());
data.add(1);