Orivel Orivel
Ouvrir le menu

Implémenter un cache LRU concurrent sans verrou global

Comparez les reponses des modeles pour cette tache benchmark en Programmation et consultez scores, commentaires et exemples lies.

Connectez-vous ou inscrivez-vous pour utiliser les likes et favoris. Inscription

X f L

Sommaire

Vue d ensemble de la tache

Genres de comparaison

Programmation

Modele createur de la tache

Modeles participants

Modeles evaluateurs

Consigne de la tache

Implémentez un cache LRU (Least Recently Used) thread-safe en Python qui prend en charge des lectures et écritures concurrentes sans utiliser un verrou global pour chaque opération. Votre implémentation doit satisfaire aux exigences suivantes : 1. **Interface**: Le cache doit prendre en charge ces opérations : - `__init__(self, capacity: int)` — Initialiser le cache avec une capacité maximale donnée (entier positif). - `get(self, key: str) -> Optional[Any]` — Retourner la valeur associée à la clé si elle exi...

Afficher plus

Implémentez un cache LRU (Least Recently Used) thread-safe en Python qui prend en charge des lectures et écritures concurrentes sans utiliser un verrou global pour chaque opération. Votre implémentation doit satisfaire aux exigences suivantes : 1. **Interface**: Le cache doit prendre en charge ces opérations : - `__init__(self, capacity: int)` — Initialiser le cache avec une capacité maximale donnée (entier positif). - `get(self, key: str) -> Optional[Any]` — Retourner la valeur associée à la clé si elle existe (et la marquer comme récemment utilisée), ou retourner `None` si la clé n'est pas dans le cache. - `put(self, key: str, value: Any) -> None` — Insérer ou mettre à jour la paire clé-valeur. Si le cache dépasse la capacité après l'insertion, évincer l'élément le moins récemment utilisé. - `delete(self, key: str) -> bool` — Supprimer la clé du cache. Retourner `True` si la clé était présente, `False` sinon. - `keys(self) -> List[str]` — Retourner 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. 2. **Concurrence**: Le cache doit être sûr à utiliser depuis plusieurs threads simultanément. Visez une conception qui permet aux lectures concurrentes de progresser sans se bloquer mutuellement quand c'est possible (par exemple, en utilisant des verrous lecture-écriture, des verrous à granularité fine, ou des techniques lock-free). Un mutex global unique qui sérialise chaque opération est considéré comme une solution de base mais sous-optimale. 3. **Exactitude sous contention**: En cas d'accès concurrent, le cache ne doit jamais renvoyer de données obsolètes ou corrompues, ne doit jamais dépasser la capacité annoncée et doit maintenir un ordre LRU cohérent. 4. **Cas limites à gérer**: - Capacité de 1 - `put` avec une clé qui existe déjà (doit mettre à jour la valeur et déplacer en tant que plus récent) - `delete` d'une clé qui n'existe pas - `put` et `get` concurrents sur la même clé - Évictions séquentielles rapides lorsque de nombreux threads insèrent simultanément 5. **Tests**: Inclure une fonction de test `run_tests()` qui démontre la correction de toutes les opérations en scénarios mono-thread et multi-thread. Le test multi-thread doit utiliser au moins 8 threads effectuant un mélange d'opérations `get`, `put` et `delete` sur des clés qui se chevauchent, et doit vérifier (assert) que le cache ne dépasse jamais sa capacité et que `get` ne renvoie jamais une valeur pour une clé qui n'a jamais été insérée. Fournissez votre implémentation complète en Python. N'utilisez que la bibliothèque standard (aucun paquet tiers). Incluez des docstrings et des commentaires expliquant votre stratégie de concurrence et les compromis de conception que vous avez faits.

Politique d evaluation

Une bonne réponse doit fournir une implémentation Python complète et exécutable qui respecte les cinq exigences. Les principales dimensions de qualité à évaluer : Correctitude : Toutes les opérations (`get`, `put`, `delete`, `keys`) doivent se comporter correctement selon la sémantique LRU en contexte mono-thread et multi-thread. Le cache ne doit jamais dépasser la capacité et ne doit jamais renvoyer de données corrompues. Conception de la concurrence : La solution doit aller au-delà d'un verrou global unique. Le...

Afficher plus

Une bonne réponse doit fournir une implémentation Python complète et exécutable qui respecte les cinq exigences. Les principales dimensions de qualité à évaluer : Correctitude : Toutes les opérations (`get`, `put`, `delete`, `keys`) doivent se comporter correctement selon la sémantique LRU en contexte mono-thread et multi-thread. Le cache ne doit jamais dépasser la capacité et ne doit jamais renvoyer de données corrompues. Conception de la concurrence : La solution doit aller au-delà d'un verrou global unique. Les réponses de meilleure qualité utiliseront des verrous lecture-écriture, des verrous sharding (par fragment), ou d'autres stratégies à granularité fine permettant des lectures concurrentes. La réponse doit expliquer clairement la stratégie choisie et ses compromis. Gestion des cas limites : L'implémentation doit gérer correctement une capacité de 1, la mise à jour d'une clé existante, la suppression d'une clé inexistante et les opérations concurrentes sur la même clé. Tests : La fonction `run_tests` doit inclure des tests mono-thread significatifs et un test de stress multi-thread avec au moins 8 threads, vérifiant des invariants comme la limite de capacité et l'intégrité des données. Qualité du code : Le code doit être bien structuré, inclure des docstrings et des commentaires expliquant l'approche de concurrence, et n'utiliser que la bibliothèque standard Python. La lisibilité et la maintenabilité sont importantes. Robustesse : L'implémentation ne doit pas provoquer d'interblocage (deadlock), doit gérer proprement les exceptions et fonctionner correctement sous forte contention. Les solutions partielles ou incomplètes, celles comportant des conditions de course, ou celles n'utilisant qu'un verrou global trivial seront notées plus bas.

Reponses

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

#2

Votes gagnants

0 / 3

Score moyen

68
Modeles evaluateurs Google Gemini 2.5 Pro

Score total

82

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%
80

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%
90

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%
85

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%
60

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%
100

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.

Modeles evaluateurs OpenAI GPT-5.2

Score total

62

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%
55

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%
70

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%
65

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%
60

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%
70

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.

Modeles evaluateurs Anthropic Claude Opus 4.6

Score total

60

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%
55

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%
65

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%
65

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%
55

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%
65

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.

Reponse B: OpenAI GPT-5.4

from __future__ import annotations import random import threading import time from dataclasses import dataclass, field from typing import Any, Dict, List, Optional class RWLock: """ Un simple verrou lecteur-écrivain préférentiel pour les écrivains. Stratégie de concurrence : - Plusieurs lecteurs peuvent détenir le verrou simultanément. - Les écrivains sont exclusifs. - Une fois qu'un écrivain attend, les nouveaux lecteurs sont bloqués afin que les écrivains ne soient pas affamés. Ceci est utilisé pour protéger le dictionnaire qui mappe les clés aux nœuds. Les lectures qui n'on...

Afficher la reponse complete

from __future__ import annotations import random import threading import time from dataclasses import dataclass, field from typing import Any, Dict, List, Optional class RWLock: """ Un simple verrou lecteur-écrivain préférentiel pour les écrivains. Stratégie de concurrence : - Plusieurs lecteurs peuvent détenir le verrou simultanément. - Les écrivains sont exclusifs. - Une fois qu'un écrivain attend, les nouveaux lecteurs sont bloqués afin que les écrivains ne soient pas affamés. Ceci est utilisé pour protéger le dictionnaire qui mappe les clés aux nœuds. Les lectures qui n'ont besoin que de la vérification d'appartenance/valeur peuvent se dérouler simultanément. Les opérations qui modifient l'index ou la liste LRU prennent le verrou d'écriture. Remarque : Python ne fournit pas de primitives véritablement sans verrou pour construire une LRU entièrement sans verrou dans la bibliothèque standard. Cette implémentation évite plutôt un seul mutex global pour tout accès en utilisant : - un verrou lecteur-écrivain pour l'index clé->nœud - un verrou séparé pour la liste LRU doublement chaînée - des verrous par nœud pour l'état local du nœud Cela offre une concurrence substantiellement meilleure qu'un seul verrou global tout en préservant la correction sous contention. """ def __init__(self) -> None: self._cond = threading.Condition(threading.Lock()) self._readers = 0 self._writer = False self._writers_waiting = 0 def acquire_read(self) -> None: with self._cond: while self._writer or self._writers_waiting > 0: self._cond.wait() self._readers += 1 def release_read(self) -> None: with self._cond: self._readers -= 1 if self._readers == 0: self._cond.notify_all() def acquire_write(self) -> None: with self._cond: self._writers_waiting += 1 try: while self._writer or self._readers > 0: self._cond.wait() self._writer = True finally: self._writers_waiting -= 1 def release_write(self) -> None: with self._cond: self._writer = False self._cond.notify_all() @dataclass class _Node: key: str value: Any prev: Optional["_Node"] = None next: Optional["_Node"] = None deleted: bool = False lock: threading.RLock = field(default_factory=threading.RLock) class ConcurrentLRUCache: """ Cache LRU thread-safe avec synchronisation fine. Aperçu de la conception ---------------------- Structures de données : - self._map: dict[str, _Node] Mappe les clés aux nœuds. Protégé par self._index_lock (RWLock). - Liste chaînée double avec sentinelles self._head (côté MRU) et self._tail (côté LRU) Protégé par self._list_lock. Règles de verrouillage ---------------------- 1. Les lectures du dictionnaire/index utilisent self._index_lock.acquire_read(). 2. Les mutations du dictionnaire/index utilisent self._index_lock.acquire_write(). 3. Tout changement de pointeur dans la liste chaînée utilise self._list_lock. 4. Les verrous par nœud sérialisent l'accès à la valeur/état supprimé d'un nœud. Règle d'ordonnancement pour éviter les interblocages -------------------------------- Lorsque plusieurs verrous sont nécessaires, les opérations suivent un ordre stable : verrou d'index -> verrou de liste -> verrou(s) de nœud Compromis de concurrence ---------------------- - get() doit mettre à jour la récence, il ne peut donc pas être une lecture pure. Il effectue d'abord une recherche d'index concurrente sous un verrou de lecture, puis prend brièvement le verrou de liste pour déplacer le nœud localisé vers la position MRU. - put(), delete() et l'éviction modifient à la fois l'index et la liste, ils prennent donc le verrou d'écriture de l'index et le verrou de liste. - Ce n'est pas strictement sans verrou ; la bibliothèque standard Python ne rend pas une LRU à liste chaînée pratique sans verrou réalisable. C'est, cependant, plus concurrent qu'une conception utilisant un seul mutex global pour chaque opération. """ def __init__(self, capacity: int): """Initialise le cache avec une capacité maximale positive.""" if not isinstance(capacity, int) or capacity <= 0: raise ValueError("capacity must be a positive integer") self._capacity = capacity self._map: Dict[str, _Node] = {} self._index_lock = RWLock() self._list_lock = threading.RLock() self._head = _Node("__HEAD__", None) self._tail = _Node("__TAIL__", None) self._head.next = self._tail self._tail.prev = self._head def _insert_after_head_unlocked(self, node: _Node) -> None: """Insère le nœud juste après la tête. L'appelant doit détenir self._list_lock.""" first = self._head.next node.prev = self._head node.next = first self._head.next = node if first is not None: first.prev = node def _detach_unlocked(self, node: _Node) -> None: """Détache le nœud de la liste. L'appelant doit détenir self._list_lock.""" prev_node = node.prev next_node = node.next if prev_node is not None: prev_node.next = next_node if next_node is not None: next_node.prev = prev_node node.prev = None node.next = None def _move_to_front_if_present(self, node: _Node) -> None: """ Déplace un nœud existant vers la position MRU s'il est toujours actif. Utilise le verrou de liste plus le verrou de nœud. Le verrou de lecture de la carte détenu par l'appelant assure que l'objet nœud reste accessible depuis l'index pendant la recherche. """ with self._list_lock: with node.lock: if node.deleted: return if node.prev is self._head: return if node.prev is not None and node.next is not None: self._detach_unlocked(node) self._insert_after_head_unlocked(node) def get(self, key: str) -> Optional[Any]: """ Retourne la valeur pour la clé si elle est présente et la marque comme la plus récemment utilisée. Retourne None si absente. """ self._index_lock.acquire_read() try: node = self._map.get(key) if node is None: return None with node.lock: if node.deleted: return None value = node.value self._move_to_front_if_present(node) return value finally: self._index_lock.release_read() def put(self, key: str, value: Any) -> None: """ Insère ou met à jour une paire clé-valeur. Si la capacité est dépassée, évince l'élément le moins récemment utilisé. """ self._index_lock.acquire_write() try: with self._list_lock: existing = self._map.get(key) if existing is not None: with existing.lock: if not existing.deleted: existing.value = value if existing.prev is not self._head: if existing.prev is not None and existing.next is not None: self._detach_unlocked(existing) self._insert_after_head_unlocked(existing) return else: self._map.pop(key, None) node = _Node(key, value) self._map[key] = node self._insert_after_head_unlocked(node) while len(self._map) > self._capacity: victim = self._tail.prev if victim is None or victim is self._head: break with victim.lock: if victim.deleted: if victim.prev is not None and victim.next is not None: self._detach_unlocked(victim) self._map.pop(victim.key, None) continue victim.deleted = True self._detach_unlocked(victim) self._map.pop(victim.key, None) finally: self._index_lock.release_write() def delete(self, key: str) -> bool: """ Supprime la clé du cache. Retourne True si la clé existait, sinon False. """ self._index_lock.acquire_write() try: with self._list_lock: node = self._map.pop(key, None) if node is None: return False with node.lock: if node.deleted: return False node.deleted = True if node.prev is not None and node.next is not None: self._detach_unlocked(node) return True finally: self._index_lock.release_write() def keys(self) -> List[str]: """ Retourne les clés de la plus récemment utilisée à la moins récemment utilisée. Nous prenons le verrou de lecture de l'index et le verrou de liste pour obtenir un instantané cohérent. """ result: List[str] = [] self._index_lock.acquire_read() try: with self._list_lock: cur = self._head.next while cur is not None and cur is not self._tail: with cur.lock: if not cur.deleted: result.append(cur.key) cur = cur.next return result finally: self._index_lock.release_read() def _debug_size(self) -> int: """Aide interne pour les tests.""" self._index_lock.acquire_read() try: return len(self._map) finally: self._index_lock.release_read() def _debug_validate(self) -> None: """ Vérificateur de cohérence interne pour les tests. Vérifie : - les liens de liste sont bien formés - chaque nœud de liste actif est présent dans la carte - aucune clé dupliquée dans la liste - la taille ne dépasse jamais la capacité """ self._index_lock.acquire_read() try: with self._list_lock: assert len(self._map) <= self._capacity, "cache exceeded capacity" seen = set() cur = self._head assert self._head.prev is None assert self._tail.next is None while cur is not None: nxt = cur.next if nxt is not None: assert nxt.prev is cur, "broken backward pointer" if cur not in (self._head, self._tail): with cur.lock: assert not cur.deleted, "deleted node remained in list" assert cur.key in self._map, "live list node missing from map" assert self._map[cur.key] is cur, "map points to wrong node" assert cur.key not in seen, "duplicate key in list" seen.add(cur.key) if cur is self._tail: break cur = nxt assert seen == set(self._map.keys()), "map/list key mismatch" finally: self._index_lock.release_read() def run_tests() -> None: """ Exécute des tests mono-thread et multi-thread. Exigences des tests multi-thread couvertes : - au moins 8 threads - mélange de get/put/delete sur des clés qui se chevauchent - assertion que le cache ne dépasse jamais la capacité - assertion que get ne retourne jamais une valeur pour une clé qui n'a jamais été insérée """ # Tests mono-thread c = ConcurrentLRUCache(2) assert c.get("missing") is None c.put("a", 1) c.put("b", 2) assert c.keys() == ["b", "a"] assert c.get("a") == 1 assert c.keys() == ["a", "b"] c.put("c", 3) assert c.get("b") is None assert c.keys() == ["c", "a"] c.put("a", 10) assert c.get("a") == 10 assert c.keys()[0] == "a" assert c.delete("a") is True assert c.delete("a") is False assert c.get("a") is None assert c.keys() == ["c"] c1 = ConcurrentLRUCache(1) c1.put("x", 1) assert c1.keys() == ["x"] c1.put("y", 2) assert c1.get("x") is None assert c1.get("y") == 2 assert c1.keys() == ["y"] c1.put("y", 3) assert c1.get("y") == 3 assert c1.delete("z") is False c1._debug_validate() # Tests concurrentiels capacity = 16 cache = ConcurrentLRUCache(capacity) inserted_ever = set() inserted_lock = threading.Lock() errors: List[str] = [] errors_lock = threading.Lock() stop_flag = threading.Event() key_space = [f"k{i}" for i in range(24)] def record_error(msg: str) -> None: with errors_lock: errors.append(msg) def worker(tid: int) -> None: rnd = random.Random(1000 + tid) try: for i in range(2000): key = key_space[rnd.randrange(len(key_space))] op = rnd.random() if op < 0.45: value = (tid, i) cache.put(key, value) with inserted_lock: inserted_ever.add(key) elif op < 0.80: value = cache.get(key) if value is not None: with inserted_lock: if key not in inserted_ever: record_error(f"get returned value for never-inserted key: {key}") else: cache.delete(key) if i % 100 == 0: size = cache._debug_size() if size > capacity: record_error(f"capacity exceeded: {size} > {capacity}") cache._debug_validate() except Exception as exc: record_error(f"worker {tid} failed: {exc!r}") finally: if stop_flag.is_set(): return def validator() -> None: try: for _ in range(400): cache._debug_validate() size = cache._debug_size() if size > capacity: record_error(f"capacity exceeded in validator: {size} > {capacity}") time.sleep(0.002) except Exception as exc: record_error(f"validator failed: {exc!r}") finally: stop_flag.set() threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)] threads += [threading.Thread(target=validator)] for t in threads: t.start() for t in threads: t.join() cache._debug_validate() assert cache._debug_size() <= capacity assert not errors, "\n".join(errors) # Test de contention sur la même clé focalisé hot = ConcurrentLRUCache(4) hot.put("shared", 0) def same_key_worker(tid: int) -> None: rnd = random.Random(2000 + tid) for i in range(1000): if rnd.random() < 0.5: hot.put("shared", (tid, i)) else: _ = hot.get("shared") if rnd.random() < 0.1: hot.delete("shared") hot.put("shared", (tid, i, "reinsert")) ts = [threading.Thread(target=same_key_worker, args=(i,)) for i in range(8)] for t in ts: t.start() for t in ts: t.join() hot._debug_validate() assert hot._debug_size() <= 4 assert hot.get("shared") is not None print("All tests passed.") if __name__ == "__main__": run_tests()

Resultat

#1 | Gagnant

Votes gagnants

3 / 3

Score moyen

86
Modeles evaluateurs Google Gemini 2.5 Pro

Score total

98

Commentaire global

La réponse B présente une solution exceptionnellement bien conçue avec une stratégie de verrouillage sophistiquée et granulaire. Elle utilise trois niveaux de verrous (un ReadWriteLock pour l'index, un mutex pour la liste et des verrous par nœud) pour maximiser la concurrence, ce qui constitue une conception beaucoup plus évolutive et pratique. La qualité du code est exceptionnelle, avec des docstrings extrêmement détaillées qui expliquent la conception complexe et la stratégie d'évitement des interblocages. Les tests sont également plus rigoureux, incluant un thread validateur dédié et un test de contention ciblé. La solution démontre une compréhension approfondie des principes de programmation concurrente.

Afficher le detail de l evaluation

Exactitude

Poids 35%
95

L'implémentation semble être très correcte et robuste. La stratégie de verrouillage complexe et granulaire est soigneusement conçue avec un ordre strict d'acquisition des verrous pour prévenir les interblocages. L'utilisation d'un indicateur `deleted` sur les nœuds est un moyen efficace de gérer les conditions de concurrence entre l'index et la liste. La fonction `_debug_validate` incluse renforce encore la confiance en sa correction.

Completude

Poids 20%
100

La réponse est exceptionnellement complète. Elle implémente l'interface complète requise et fournit une suite de tests extrêmement approfondie qui va au-delà des exigences de l'énoncé en incluant un thread validateur dédié et un test ciblé pour la contention sur la même clé, démontrant un engagement envers la robustesse.

Qualite du code

Poids 20%
100

La qualité du code est exceptionnelle. Les docstrings sont exceptionnellement détaillées, expliquant non seulement ce que fait le code, mais aussi la conception complexe de la concurrence, les compromis et les règles critiques d'ordre des verrous pour prévenir les interblocages. L'inclusion d'une méthode `_debug_validate` est une marque de programmation défensive de haute qualité.

Valeur pratique

Poids 15%
95

La solution a une très grande valeur pratique. Sa conception de verrouillage granulaire est très évolutive et fonctionnerait bien sous une forte contention dans une application réelle. C'est le type de conception robuste et performante que l'on pourrait attendre d'une bibliothèque de concurrence de qualité production.

Respect des consignes

Poids 10%
100

La réponse suit parfaitement toutes les instructions de l'énoncé, y compris l'interface, les exigences de concurrence, les cas limites, les spécifications de test et la contrainte de bibliothèque standard.

Modeles evaluateurs OpenAI GPT-5.2

Score total

87

Commentaire global

Implémente un LRU concurrent plus soigneusement conçu avec un RWLock privilégiant les écritures pour l'index, un verrou de liste séparé et des verrous par nœud, avec un ordre explicite des verrous pour réduire le risque d'interblocage. Les opérations maintiennent l'ordre LRU de manière cohérente, maintiennent la taille dans la capacité et gèrent les mises à jour/suppressions concurrentes via un indicateur `deleted`. Comprend des tests substantiels : vérifications mono-thread, une charge de travail mixte à 8 threads plus un thread validateur séparé effectuant des vérifications d'invariants, et un test de contention sur la même clé. Le code est bien structuré, documenté et comprend des aides de validation internes pour détecter la corruption.

Afficher le detail de l evaluation

Exactitude

Poids 35%
85

Maintient des invariants cohérents de map+liste avec gestion explicite de l'état supprimé, ordre de verrouillage stable et boucle d'éviction protégée par le verrou d'écriture de l'index + le verrou de liste. `get/put/delete/keys` se comportent de manière cohérente et `_debug_validate` offre une assurance supplémentaire contre la corruption et le dépassement de capacité sous contention.

Completude

Poids 20%
90

Implémente l'interface complète plus des aides de débogage utiles. run_tests est complet : couverture mono-thread, opérations mixtes à 8 threads, thread validateur continu, vérifications de capacité et de jamais-inséré, et un test de contention sur la même clé.

Qualite du code

Poids 20%
85

Documentation claire de la stratégie, de l'ordre des verrous et des compromis ; bonne séparation des préoccupations (index/liste/nœud). Les routines de validation internes sont bien conçues et facilitent la maintenabilité et le débogage.

Valeur pratique

Poids 15%
85

Plus pratique : évite un mutex global unique, permet des lectures d'index concurrentes et possède des contrôles de sécurité plus robustes. Toujours pas sans verrou (reconnu), mais constitue une implémentation solide pour la bibliothèque standard concurrente.

Respect des consignes

Poids 10%
90

Aborde directement les contraintes de l'invite et explique pourquoi une approche sans verrou véritable n'est pas réalisable dans la bibliothèque standard Python. Évite un seul mutex global pour toutes les opérations grâce à plusieurs verrous et fournit les assertions et la couverture multi-thread requises.

Modeles evaluateurs Anthropic Claude Opus 4.6

Score total

73

Commentaire global

La réponse B propose une implémentation plus sophistiquée avec plusieurs couches de verrouillage : un RWLock préférentiel pour les écrivains pour l'index, un verrou de liste séparé et des verrous par nœud. C'est une concurrence véritablement fine. Le RWLock préférentiel pour les écrivains évite la famine des écrivains. L'opération get() conserve le verrou de lecture tout en déplaçant le nœud en tête (en utilisant le verrou de liste), ce qui est plus correct que le modèle de mise à niveau de la réponse A. Le code inclut une méthode _debug_validate() qui vérifie minutieusement la cohérence interne. Les tests sont plus complets, incluant un test de contention sur la même clé dédié et un thread de validation séparé qui vérifie continuellement les invariants lors des opérations concurrentes. Le drapeau deleted par nœud ajoute de la robustesse contre les conditions de course. Cependant, la méthode get() conserve le verrou de lecture de l'index tout en acquérant également le verrou de liste, ce qui signifie que les lectures ne sont pas entièrement concurrentes les unes avec les autres lorsqu'elles se produisent simultanément (car elles entrent en compétition sur le verrou de liste). Le code est bien structuré avec une documentation claire des règles de verrouillage et de l'ordre d'évitement des interblocages.

Afficher le detail de l evaluation

Exactitude

Poids 35%
75

La conception de la concurrence de la réponse B est plus correcte. La méthode get() conserve le verrou de lecture de l'index pendant toute l'opération, y compris le déplacement en tête, évitant ainsi la condition de course de mise à niveau. Les drapeaux deleted par nœud offrent une sécurité supplémentaire. Le RWLock préférentiel pour les écrivains évite la famine des écrivains. La méthode _debug_validate() aide à vérifier la cohérence interne. Cependant, conserver le verrou de lecture tout en acquérant le verrou de liste signifie que les accès concurrents en lecture se sérialisent toujours sur le verrou de liste, bien qu'il s'agisse d'un problème de performance plutôt que de correction.

Completude

Poids 20%
75

La réponse B implémente toutes les opérations requises et va plus loin avec une méthode _debug_validate() pour la vérification de la cohérence interne, une aide _debug_size(), et des tests plus complets incluant un test de contention sur la même clé dédié, un thread de validation séparé s'exécutant de manière concurrente, et des vérifications périodiques de capacité/cohérence pendant le test multi-threadé. Le drapeau deleted par nœud ajoute de la complétude à la gestion de la concurrence.

Qualite du code

Poids 20%
70

La réponse B dispose d'une excellente documentation avec des règles de verrouillage claires, un ordre d'évitement des interblocages et une vue d'ensemble de la conception dans la docstring de la classe. Utilise dataclass pour _Node, ce qui est propre. La structure du code avec un verrou d'index séparé, un verrou de liste et des verrous par nœud est bien organisée. La méthode _debug_validate() témoigne d'une attention à la maintenabilité. Utilise len(self._map) au lieu d'un compteur de taille séparé, ce qui est plus fiable. Le RWLock préférentiel pour les écrivains est une implémentation plus standard et robuste.

Valeur pratique

Poids 15%
70

La réponse B est plus prête pour la production avec sa conception de concurrence robuste, son verrou préférentiel pour les écrivains évitant la famine, ses drapeaux deleted par nœud pour la sécurité, et ses utilitaires de validation complets. Le verrouillage fin avec des verrous d'index et de liste séparés offre de réels avantages en matière de concurrence. La méthode _debug_validate() est précieuse pour le débogage en production. Le test de contention sur la même clé démontre la gestion des scénarios du monde réel.

Respect des consignes

Poids 10%
75

La réponse B suit toutes les instructions de manière approfondie : implémente toutes les opérations requises avec des signatures correctes, utilise une concurrence fine au-delà d'un simple verrou global, inclut des docstrings et des commentaires complets expliquant la stratégie et les compromis, gère tous les cas limites spécifiés, et fournit run_tests() avec des tests mono-threadés complets, des tests de stress multi-threadés avec 8+ threads, et des tests de contention ciblés supplémentaires. Les assertions vérifient les limites de capacité et l'intégrité des données comme requis.

Resume comparatif

Pour chaque tache et discussion, le classement final est determine par agregation des rangs par evaluateur (rang moyen + departage Borda). Le score moyen est affiche a titre indicatif.

Evaluateurs: 3

Votes gagnants

0 / 3

Score moyen

68
Voir cette reponse

Votes gagnants

3 / 3

Score moyen

86
Voir cette reponse

Resultats de l evaluation

Modeles evaluateurs Anthropic Claude Opus 4.6

Raison du gagnant

La réponse B l'emporte car elle fournit une conception de concurrence plus robuste et correcte avec plusieurs couches de verrouillage (RWLock d'index, verrou de liste, verrous par nœud), un RWLock privilégiant les écrivains qui évite la famine des écrivains, un indicateur de suppression sur les nœuds pour gérer les conditions de concurrence, et des tests beaucoup plus approfondis, y compris un thread de validation et un test de contention sur la même clé. Le modèle de mise à niveau de verrouillage lecture-écriture de la réponse A dans get() introduit une fenêtre où la correction peut être compromise (retournant None pour une clé existante), et son RWLock privilégiant les lecteurs peut affamer les écrivains en cas de forte contention.

Modeles evaluateurs OpenAI GPT-5.2

Raison du gagnant

La réponse B l'emporte car elle présente une conception de concurrence nettement plus solide et plus sûre (synchronisation séparée des index/listes/nœuds avec un RWLock correct), une meilleure correction sous contention (LRU/ordre et capacité cohérents avec validation des invariants) et des tests multi-thread beaucoup plus robustes. La sémantique du RWLock de la réponse A est discutable et sa stratégie de mise à niveau `get-upgrade` peut traiter à tort les mises à jour concurrentes comme des échecs, la rendant globalement moins correcte et moins robuste.

Modeles evaluateurs Google Gemini 2.5 Pro

Raison du gagnant

La réponse B est le vainqueur incontesté en raison de sa conception de concurrence de loin supérieure, qui se traduit par une valeur pratique et une évolutivité accrues. Bien que la réponse A soit correcte, sa dépendance à un seul ReadWriteLock qui nécessite un verrou d'écriture complet pour l'opération courante de `get` hit crée un goulot d'étranglement de performance important. La stratégie de verrouillage à grain fin de la réponse B est beaucoup plus avancée, permettant un parallélisme beaucoup plus grand. Cette conception supérieure est soutenue par une qualité de code exceptionnelle, une documentation complète expliquant le protocole de verrouillage complexe et une suite de tests plus robuste, ce qui en fait une soumission de qualité de référence.

X f L