Reponse A: Google Gemini 2.5 Flash
import threading import random import time from typing import Any, Optional, List class _Node: """ Représente un nœud dans la liste doublement chaînée utilisée pour l'ordre LRU. Utilise __slots__ pour l'efficacité de la mémoire. """ __slots__ = 'key', 'value', 'prev', 'next' def __init__(self, key: str, value: Any): self.key = key self.value = value self.prev = None self.next = None class ReadWriteLock: """ Une implémentation personnalisée de verrou Lecture-Écriture. Ce verrou permet à plusieurs lecteurs de détenir le verrou simultanément, mai...
Afficher la reponse complete ▼
import threading import random import time from typing import Any, Optional, List class _Node: """ Représente un nœud dans la liste doublement chaînée utilisée pour l'ordre LRU. Utilise __slots__ pour l'efficacité de la mémoire. """ __slots__ = 'key', 'value', 'prev', 'next' def __init__(self, key: str, value: Any): self.key = key self.value = value self.prev = None self.next = None class ReadWriteLock: """ Une implémentation personnalisée de verrou Lecture-Écriture. Ce verrou permet à plusieurs lecteurs de détenir le verrou simultanément, mais à un seul écrivain à la fois. Un écrivain doit attendre que tous les lecteurs aient terminé, et les lecteurs doivent attendre qu'un écrivain ait terminé. Il s'agit d'une implémentation privilégiant les lecteurs, ce qui signifie que les nouveaux lecteurs peuvent acquérir le verrou de lecture même si un écrivain attend. Cette classe fournit également des gestionnaires de contexte pour une acquisition et une libération plus propres du verrou. """ def __init__(self): self._read_lock = threading.Lock() # Protège le compteur _readers et la variable de condition self._write_lock = threading.Lock() # Le verrou d'écriture réel self._readers = 0 # Nombre de lecteurs actifs self._no_readers = threading.Condition(self._read_lock) # Condition pour que les écrivains attendent les lecteurs def acquire_read(self): """Acquiert un verrou de lecture. Plusieurs lecteurs peuvent détenir ce verrou simultanément.""" with self._read_lock: # Attend si un écrivain détient actuellement le verrou d'écriture. # Les nouveaux lecteurs seront bloqués si un écrivain est actif. while self._write_lock.locked(): self._no_readers.wait() self._readers += 1 def release_read(self): """Libère un verrou de lecture.""" with self._read_lock: self._readers -= 1 # S'il ne reste plus de lecteurs, notifier les écrivains en attente if self._readers == 0: self._no_readers.notify_all() def acquire_write(self): """Acquiert un verrou d'écriture. Un seul écrivain peut détenir ce verrou à la fois.""" # Acquiert d'abord le verrou d'écriture principal pour signaler l'intention d'écrire self._write_lock.acquire() with self._read_lock: # Puis acquiert le verrou de lecture pour attendre les lecteurs actifs # Attend jusqu'à ce que tous les lecteurs actifs aient terminé while self._readers > 0: self._no_readers.wait() def release_write(self): """Libère un verrou d'écriture.""" with self._read_lock: # Acquiert le verrou de lecture pour notifier les lecteurs/écrivains potentiels self._no_readers.notify_all() # Notifie les lecteurs ou écrivains en attente self._write_lock.release() # Libère le verrou d'écriture principal class _ReadContextManager: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.acquire_read() def __exit__(self, exc_type, exc_val, exc_tb): self.lock.release_read() class _WriteContextManager: def __init__(self, lock): self.lock = lock def __enter__(self): self.lock.acquire_write() def __exit__(self, exc_type, exc_val, exc_tb): self.lock.release_write() def read_locked(self): """Retourne un gestionnaire de contexte pour acquérir un verrou de lecture.""" return self._ReadContextManager(self) def write_locked(self): """Retourne un gestionnaire de contexte pour acquérir un verrou d'écriture.""" return self._WriteContextManager(self) class LRUCache: """ Une implémentation de cache LRU (Least Recently Used) thread-safe. Ce cache utilise un ReadWriteLock pour gérer l'accès concurrent. La conception vise à permettre des lectures concurrentes pour les misses de cache et l'opération `keys()`. Cependant, les opérations qui modifient l'état du cache ou l'ordre LRU (`put`, `delete`, et les hits de cache dans `get`) acquièrent un verrou d'écriture, garantissant l'atomicité et la cohérence pour ces sections critiques. Cette approche offre une meilleure concurrence qu'un seul mutex global pour toutes les opérations, en particulier pour les charges de travail fortement axées sur la lecture avec des misses de cache fréquents ou des appels à `keys()`. L'opération `get` utilise un modèle de "mise à niveau" : elle tente d'abord un verrou de lecture pour vérifier l'existence de la clé. Si elle est trouvée, elle libère le verrou de lecture et acquiert un verrou d'écriture pour mettre à jour l'ordre LRU et récupérer la dernière valeur. Ce modèle est soigneusement implémenté pour gérer les conditions de concurrence qui pourraient survenir lors de la mise à niveau du verrou (par exemple, la clé étant supprimée ou mise à jour par un autre thread). """ def __init__(self, capacity: int): if capacity <= 0: raise ValueError("La capacité doit être un entier positif.") self._capacity = capacity self._cache = {} # Stocke clé -> _Node pour une recherche en O(1) self._size = 0 # Nœuds tête et queue factices pour la liste doublement chaînée. # Cela simplifie les cas limites pour l'ajout/suppression de nœuds, car les nœuds de données réels # sont toujours entre la tête et la queue. self._head = _Node("dummy_head", None) self._tail = _Node("dummy_tail", None) self._head.next = self._tail self._tail.prev = self._head # Contrôle de concurrence : un seul ReadWriteLock protège l'état complet du cache. self._rw_lock = ReadWriteLock() def _add_node(self, node: _Node): """Ajoute un nœud juste après la tête (position la plus récemment utilisée). Suppose que le verrou d'écriture est détenu.""" node.prev = self._head node.next = self._head.next self._head.next.prev = node self._head.next = node def _remove_node(self, node: _Node): """Supprime un nœud de la liste doublement chaînée. Suppose que le verrou d'écriture est détenu.""" prev_node = node.prev next_node = node.next prev_node.next = next_node next_node.prev = prev_node def _move_to_front(self, node: _Node): """Déplace un nœud existant vers l'avant de la liste (le plus récemment utilisé). Suppose que le verrou d'écriture est détenu.""" self._remove_node(node) self._add_node(node) def _pop_tail(self) -> _Node: """Supprime et retourne le nœud le moins récemment utilisé (celui juste avant la queue). Suppose que le verrou d'écriture est détenu.""" node = self._tail.prev self._remove_node(node) return node def get(self, key: str) -> Optional[Any]: """ Retourne la valeur associée à la clé si elle existe, et la marque comme récemment utilisée. Retourne None si la clé n'est pas dans le cache. Cette opération acquiert d'abord un verrou de lecture pour vérifier l'existence de la clé. Si la clé est trouvée, elle libère le verrou de lecture puis acquiert un verrou d'écriture pour mettre à jour l'ordre LRU et récupérer la dernière valeur. Ce "modèle de mise à niveau" permet des opérations `get` concurrentes manquantes et des opérations `keys()`, mais sérialise les `get` réussis avec les autres écritures. """ node_found_under_read_lock = None with self._rw_lock.read_locked(): node_found_under_read_lock = self._cache.get(key) if not node_found_under_read_lock: return None # Si la clé est trouvée, acquiert le verrou d'écriture pour mettre à jour l'ordre LRU et obtenir la dernière valeur. # C'est un "modèle de mise à niveau". Une condition de concurrence peut survenir ici : un autre thread # pourrait supprimer ou mettre à jour la clé entre la libération du verrou de lecture et l'acquisition du verrou d'écriture. with self._rw_lock.write_locked(): # Revérifie si la clé existe toujours et fait référence au même objet nœud. # Ceci est crucial pour la correction sous contention. Si un autre thread a effectué # un `put` pour la même clé, `_cache.get(key)` retournerait un *nouveau* nœud. # Si un autre thread a effectué un `delete`, il retournerait `None`. current_node = self._cache.get(key) if current_node is node_found_under_read_lock: # S'assure que c'est le même objet nœud self._move_to_front(node_found_under_read_lock) value = current_node.value # Obtient la dernière valeur après le déplacement return value else: # Le nœud a été soit supprimé, soit remplacé par une autre opération `put` # entre l'acquisition de notre verrou de lecture et de verrou d'écriture. Traite comme un miss. return None def put(self, key: str, value: Any) -> None: """ Insère ou met à jour la paire clé-valeur. Si le cache dépasse la capacité après l'insertion, il évince l'élément le moins récemment utilisé. C'est une opération d'écriture et elle acquiert un verrou d'écriture. """ with self._rw_lock.write_locked(): node = self._cache.get(key) if node: # La clé existe : met à jour la valeur et déplace vers l'avant node.value = value self._move_to_front(node) else: # La clé n'existe pas : ajoute un nouveau nœud new_node = _Node(key, value) self._cache[key] = new_node self._add_node(new_node) self._size += 1 # Évince si la capacité est dépassée if self._size > self._capacity: lru_node = self._pop_tail() del self._cache[lru_node.key] self._size -= 1 def delete(self, key: str) -> bool: """ Supprime la clé du cache. Retourne True si la clé était présente, False sinon. C'est une opération d'écriture et elle acquiert un verrou d'écriture. """ with self._rw_lock.write_locked(): node = self._cache.get(key) if node: self._remove_node(node) del self._cache[key] self._size -= 1 return True return False def keys(self) -> List[str]: """ Retourne une liste de toutes les clés actuellement dans le cache, ordonnées de la plus récemment utilisée à la moins récemment utilisée. C'est une opération de lecture et elle acquiert un verrou de lecture. """ with self._rw_lock.read_locked(): keys_list = [] current = self._head.next while current is not self._tail: keys_list.append(current.key) current = current.next return keys_list def run_tests(): """ Exécute une série de tests mono-thread et multi-thread pour vérifier l'implémentation LRUCache. """ print("--- Démarrage des tests du cache LRU ---") # --- Tests mono-thread --- print("\n--- Tests mono-thread ---") cache = LRUCache(capacity=3) assert cache.get("a") is None, "ST: Obtenir une clé inexistante doit retourner None" assert cache.keys() == [], "ST: Un cache vide doit avoir une liste de clés vide" cache.put("a", 1) assert cache.get("a") == 1, "ST: Obtenir une clé existante après put" assert cache.keys() == ["a"], "ST: Liste des clés après un put" cache.put("b", 2) cache.put("c", 3) assert cache.keys() == ["c", "b", "a"], "ST: Liste des clés après plusieurs puts" assert cache._size == 3, "ST: La taille du cache doit être 3" # Test d'éviction de capacité cache.put("d", 4) # 'a' doit être évincé car c'est le LRU assert cache.get("a") is None, "ST: La clé évincée 'a' doit être None" assert cache.get("d") == 4, "ST: La nouvelle clé 'd' doit être présente" assert cache.keys() == ["d", "c", "b"], "ST: Liste des clés après éviction" assert cache._size == 3, "ST: La taille du cache doit rester 3 après éviction" # Test de put avec clé existante (mise à jour de la valeur et déplacement vers l'avant) cache.put("b", 22) # 'b' doit se déplacer vers l'avant, valeur mise à jour assert cache.get("b") == 22, "ST: Valeur mise à jour pour 'b'" assert cache.keys() == ["b", "d", "c"], "ST: Liste des clés après mise à jour et déplacement de 'b'" # Test de suppression assert cache.delete("c") is True, "ST: Supprimer la clé existante 'c'" assert cache.get("c") is None, "ST: La clé supprimée 'c' doit être None" assert cache.keys() == ["b", "d"], "ST: Liste des clés après suppression de 'c'" assert cache._size == 2, "ST: Taille du cache après suppression" assert cache.delete("z") is False, "ST: Supprimer la clé inexistante 'z'" assert cache.keys() == ["b", "d"], "ST: Liste des clés inchangée après suppression de clé inexistante" # Test de capacité 1 cache_cap1 = LRUCache(capacity=1) cache_cap1.put("x", 10) assert cache_cap1.get("x") == 10, "ST: Capacité 1, obtenir 'x'" cache_cap1.put("y", 20) # 'x' doit être évincé assert cache_cap1.get("x") is None, "ST: Capacité 1, 'x' évincé" assert cache_cap1.get("y") == 20, "ST: Capacité 1, obtenir 'y'" assert cache_cap1.keys() == ["y"], "ST: Capacité 1, liste des clés" print("Tests mono-thread réussis !") # --- Tests multi-thread --- print("\n--- Tests multi-thread ---") MT_CAPACITY = 10 MT_NUM_THREADS = 8 MT_OPS_PER_THREAD = 2000 MT_KEY_RANGE = 20 # Clés de 0 à 19 (par exemple, "0", "1", ..., "19") mt_cache = LRUCache(capacity=MT_CAPACITY) # Barrière pour s'assurer que tous les threads de travail démarrent leurs opérations simultanément barrier = threading.Barrier(MT_NUM_THREADS + 1) # +1 pour le thread principal error_flag = threading.Event() # Défini si un thread rencontre une erreur # Suit les clés qui ont *jamais* été insérées avec succès dans le cache par un thread. # Utilisé pour vérifier que `get` ne retourne jamais une valeur pour une clé qui n'a jamais été réellement insérée. global_ever_inserted_keys = set() global_ever_inserted_keys_lock = threading.Lock() def mt_worker(cache_instance, thread_id, barrier_instance, error_event, ever_inserted_keys_set, ever_inserted_keys_lock): """Fonction de travail pour les tests multi-thread.""" barrier_instance.wait() # Attend que tous les threads soient prêts à démarrer les opérations for i in range(MT_OPS_PER_THREAD): if error_event.is_set(): break # Arrête si un autre thread a déjà trouvé une erreur # Choisit aléatoirement une opération avec un biais vers 'get' et 'put' op = random.choices(['get', 'put', 'delete'], weights=[0.5, 0.4, 0.1], k=1)[0] key = str(random.randint(0, MT_KEY_RANGE - 1)) value = f"value_T{thread_id}_K{key}_I{i}" if op == 'put': cache_instance.put(key, value) with ever_inserted_keys_lock: ever_inserted_keys_set.add(key) elif op == 'get': val = cache_instance.get(key) if val is not None: # Vérifie que si une valeur est retournée, la clé doit avoir été insérée à un moment donné. with ever_inserted_keys_lock: if key not in ever_inserted_keys_set: print(f"Erreur MT : Le thread {thread_id} a obtenu la valeur '{val}' pour la clé '{key}' " f"qui n'a jamais été insérée globalement. Cela ne devrait pas arriver.") error_event.set() break # Vérifie que la valeur n'est pas corrompue (vérification simple du format) if not isinstance(val, str) or not val.startswith("value_T"): print(f"Erreur MT : Le thread {thread_id} a obtenu une valeur corrompue '{val}' pour la clé '{key}'.") error_event.set() break elif op == 'delete': cache_instance.delete(key) # Optionnel : Introduire un petit sommeil aléatoire pour augmenter le changement de contexte et la contention # time.sleep(random.uniform(0.00001, 0.0001)) # print(f"Le thread {thread_id} a terminé.") # Décommenter pour une sortie détaillée theads = [] for i in range(MT_NUM_THREADS): thread = threading.Thread(target=mt_worker, args=(mt_cache, i, barrier, error_flag, global_ever_inserted_keys, global_ever_inserted_keys_lock)) threads.append(thread) thread.start() # Attend que tous les threads de travail atteignent la barrière et démarrent les opérations barrier.wait() print(f"Les {MT_NUM_THREADS} threads ont démarré les opérations.") # Attend que tous les threads de travail terminent leurs opérations for thread in threads: thread.join() print("Tous les threads ont terminé.") # Assertions finales pour les tests multi-thread assert not error_flag.is_set(), "MT : Des erreurs ont été détectées pendant les opérations multi-thread. Vérifiez la console pour les détails." assert mt_cache._size <= MT_CAPACITY, f"MT : La taille du cache ({mt_cache._size}) a dépassé la capacité ({MT_CAPACITY})." # Vérifie que toutes les clés restantes dans le cache ont été insérées à un moment donné final_keys = mt_cache.keys() with global_ever_inserted_keys_lock: for key in final_keys: assert key in global_ever_inserted_keys, f"Erreur MT : Le cache final contient la clé '{key}' qui n'a jamais été insérée globalement." print("Tests multi-thread réussis !") print("\n--- Tous les tests du cache LRU ont réussi ! ---") if __name__ == '__main__': run_tests()
Resultat
Votes gagnants
0 / 3
Score moyen
Score total
Commentaire global
La réponse A fournit une implémentation correcte et bien structurée d'un cache LRU concurrent. Elle utilise un `ReadWriteLock` personnalisé et un astucieux schéma de « mise à niveau de verrouillage » dans la méthode `get` pour gérer les conditions de concurrence. Le code est propre, bien commenté et comprend une suite de tests complète. Cependant, sa principale faiblesse réside dans son modèle de concurrence. Étant donné qu'un succès dans `get` nécessite l'acquisition d'un verrou d'écriture complet pour mettre à jour l'ordre LRU, les performances du cache sont sévèrement limitées dans les charges de travail à forte lecture avec un cache chaud, rendant son avantage pratique par rapport à un simple verrou global marginal.
Afficher le detail de l evaluation ▼
Exactitude
Poids 35%L'implémentation est correcte, et la gestion de la condition de concurrence dans le schéma de mise à niveau de verrouillage de la méthode `get` est saine. Cependant, le choix d'un verrou privilégiant les lecteurs pourrait entraîner une famine des écrivains, et le modèle de concurrence global présente des problèmes de performance inhérents qui frôlent un défaut de conception pour ce problème spécifique.
Completude
Poids 20%La réponse est entièrement complète. Elle implémente toutes les méthodes requises de l'interface et fournit une fonction `run_tests` avec des cas de test solides en mono-thread et multi-thread qui répondent aux exigences de l'invite.
Qualite du code
Poids 20%La qualité du code est très bonne. Le code est bien structuré, lisible et comprend des docstrings et des commentaires clairs expliquant sa stratégie de concurrence. L'utilisation de `__slots__` dans la classe `_Node` est une optimisation appréciable.
Valeur pratique
Poids 15%La valeur pratique est limitée par le modèle de concurrence. Étant donné que les succès de `get` (l'opération la plus courante sur un cache chaud) nécessitent un verrou d'écriture complet, l'implémentation sérialise la plupart des opérations et offre peu d'avantage de performance par rapport à un simple verrou global dans de nombreux scénarios réels à forte lecture.
Respect des consignes
Poids 10%La réponse suit parfaitement toutes les instructions de l'invite, y compris l'interface, les exigences de concurrence, les cas limites, les spécifications de test et la contrainte de bibliothèque standard.
Score total
Commentaire global
Fournit un cache LRU exécutable avec un RW lock personnalisé et une liste doublement chaînée. Le comportement de base en mono-thread et la gestion de la capacité semblent corrects. Cependant, la stratégie de concurrence est effectivement un verrou d'écriture global pour tous les accès (hits), ajouts (puts) et suppressions, et l'implémentation du RWLock est défectueuse : les lecteurs vérifient `write_lock.locked()` sans le détenir, et les écrivains peuvent acquérir le verrou d'écriture pendant que de nouveaux lecteurs sont encore autorisés, violant potentiellement la sémantique RW. Le chemin de « mise à niveau » du `get` peut incorrectement retourner None même si la clé existe (nœud remplacé) et peut manquer des valeurs sous contention ; tout en évitant sans doute les données obsolètes, cela sacrifie les attentes de correction pour `get` sous des ajouts concurrents sur la même clé. Les tests sont corrects mais s'appuient sur `_size` interne et ne valident pas l'intégrité de la liste/map sous contention.
Afficher le detail de l evaluation ▼
Exactitude
Poids 35%La logique LRU est globalement correcte en utilisation mono-thread, mais l'implémentation du RWLock n'est pas un verrou lecteur-rédacteur valide (le filtrage des lecteurs basé sur `write_lock.locked()`/l'utilisation de conditions est sujet aux courses). Sous contention, `get` peut retourner None pour une clé existante si elle a été mise à jour (nœud remplacé) entre les verrous, ce qui est une dégradation sémantique évitable et peut violer le comportement attendu pour des ajouts/accès concurrents sur la même clé.
Completude
Poids 20%Implémente toutes les méthodes requises et fournit `run_tests` avec des scénarios mono-thread et multi-thread utilisant 8 threads et des vérifications d'invariants (capacité, clé jamais insérée). Cependant, il manque des vérifications d'intégrité structurelle plus approfondies (cohérence liste/map) et utilise des champs internes dans les tests.
Qualite du code
Poids 20%Structure et commentaires lisibles, mais l'implémentation du RWLock est complexe et discutablement incorrecte ; utilise des champs privés dans les tests et expose `_size` qui n'est pas strictement nécessaire. Certaines explications (privilégier les lecteurs vs comportement réel) sont incohérentes avec le code.
Valeur pratique
Poids 15%Fonctionne comme un cache concurrent de base, mais les performances sous contention sont limitées car tous les accès nécessitent un verrou d'écriture et les préoccupations concernant le RWLock réduisent la confiance pour une utilisation en production.
Respect des consignes
Poids 10%Respecte les exigences de l'interface et inclut des docstrings/commentaires et un test mixte à 8 threads. Cependant, l'invite demande d'éviter un verrou global pour chaque opération ; en pratique, la plupart des opérations significatives sont sérialisées via le verrou d'écriture, et les avantages de la concurrence sont limités.
Score total
Commentaire global
La réponse A fournit une implémentation complète et exécutable avec un ReadWriteLock personnalisé et un cache LRU utilisant une liste doublement chaînée avec des nœuds sentinelles. La stratégie de concurrence utilise un seul RWLock pour l'ensemble du cache, ce qui est mieux qu'un mutex global mais reste limité. La méthode get() utilise un modèle de mise à niveau lecture puis écriture, qui présente un problème subtil : entre la libération du verrou de lecture et l'acquisition du verrou d'écriture, le nœud pourrait être supprimé et la clé réinsérée avec un nœud différent, ce qui amènerait get() à retourner None même si la clé existe (un problème de correction). L'implémentation ReadWriteLock présente un problème potentiel : elle vérifie `self._write_lock.locked()` dans acquire_read, mais il s'agit d'un verrou privilégiant les lecteurs qui pourrait affamer les écrivains. Les tests sont raisonnables mais pourraient être plus approfondis. Le code est bien documenté avec des docstrings claires expliquant la stratégie de concurrence.
Afficher le detail de l evaluation ▼
Exactitude
Poids 35%Le get() de la réponse A présente un problème de correction : le modèle de mise à niveau du verrou lecture vers écriture signifie qu'entre la libération du verrou de lecture et l'acquisition du verrou d'écriture, un autre thread pourrait supprimer et réinsérer la même clé avec un nouveau nœud. La vérification d'identité (current_node is node_found_under_read_lock) échouerait alors, provoquant le retour de None par get() même si la clé existe avec une valeur valide. L'acquire_read du ReadWriteLock vérifie _write_lock.locked() ce qui n'est pas entièrement fiable. La conception privilégiant les lecteurs peut affamer les écrivains.
Completude
Poids 20%La réponse A implémente toutes les opérations requises (get, put, delete, keys) avec une sémantique LRU appropriée. Elle gère une capacité de 1, la mise à jour des clés existantes et la suppression des clés inexistantes. Les tests couvrent les scénarios mono-thread et multi-thread avec 8 threads. Cependant, elle manque d'utilitaires de validation internes et le test multi-thread pourrait être plus approfondi.
Qualite du code
Poids 20%La réponse A a une bonne documentation avec des docstrings claires expliquant la stratégie de concurrence et les compromis. Le code est bien structuré avec une séparation nette des responsabilités. Le ReadWriteLock dispose de gestionnaires de contexte pour une utilisation propre. Utilise __slots__ sur _Node pour l'efficacité mémoire. Cependant, le suivi de la taille est manuel et pourrait dériver en cas de bugs, et l'implémentation ReadWriteLock présente des choix de conception discutables (vérification de l'état locked()).
Valeur pratique
Poids 15%La réponse A fournit un cache LRU fonctionnel mais la condition de concurrence du modèle de mise à niveau du get() réduit sa fiabilité pratique. Le RWLock privilégiant les lecteurs pourrait causer une famine des écrivains en production. L'avantage de la concurrence est limité car les accès get nécessitent toujours un verrou d'écriture, et le modèle de mise à niveau ajoute une surcharge. Nécessiterait des corrections avant une utilisation en production.
Respect des consignes
Poids 10%La réponse A suit la plupart des instructions : implémente toutes les opérations requises, utilise une stratégie de concurrence au-delà d'un simple verrou global, inclut des docstrings et des commentaires, gère les cas limites et fournit run_tests() avec des tests mono-thread et multi-thread utilisant 8 threads. Cependant, les assertions du test multi-thread pourraient être plus solides, et l'explication de la conception de la concurrence pourrait mieux aborder les compromis.