Orivel Orivel
メニューを開く

ロックフリーの並行LRUキャッシュを実装する

このプログラミングベンチマークに対する各AIの回答と比較結果を確認できます。

いいね・お気に入り機能を使うにはログインまたは新規登録が必要です。 新規登録

X f L

目次

お題概要

比較ジャンル

プログラミング

お題作成モデル

回答モデル

採点モデル

お題本文

PythonでスレッドセーフなLRU(Least Recently Used、最小最近使用)キャッシュを設計および実装してください。各操作ごとにグローバルロックを使用せずに、同時並行の読み取りおよび書き込みをサポートすること。実装は次の要件を満たす必要があります: 1. キャッシュは、コンストラクタ時に指定された固定の最大容量を持つこと。 2. 次の3つの操作をサポートすること: - get(key): キーに関連付けられた値を返す。キーが存在しない場合は None を返す。キーへのアクセスは、そのキーを最も最近使用されたものとしてマークすること。 - put(key, val...

さらに表示

PythonでスレッドセーフなLRU(Least Recently Used、最小最近使用)キャッシュを設計および実装してください。各操作ごとにグローバルロックを使用せずに、同時並行の読み取りおよび書き込みをサポートすること。実装は次の要件を満たす必要があります: 1. キャッシュは、コンストラクタ時に指定された固定の最大容量を持つこと。 2. 次の3つの操作をサポートすること: - get(key): キーに関連付けられた値を返す。キーが存在しない場合は None を返す。キーへのアクセスは、そのキーを最も最近使用されたものとしてマークすること。 - put(key, value): キーと値のペアを挿入または更新する。キャッシュが容量に達しており新しいキーを挿入する場合、最も最近使用されていない(LRU)エントリを追い出すこと。 - delete(key): キーが存在する場合はキャッシュから削除する。キーが見つかって削除された場合は True を、そうでなければ False を返すこと。 3. キャッシュは複数のスレッドから同時に安全に使用できること。異なるキーに対する同時の get 操作は互いにブロックすべきではない。競合を最小化すること — 全てに対して粗い単一のロックをかける設計は許容されない。 4. 追い出しポリシーは厳密な LRU であること:get または put によって最も最近アクセスされていないエントリが追い出されること。 5. エッジケースを扱うこと:容量が1の場合、追い出しを引き起こす急速な同時 put 操作、異なるスレッドから同一キーに対する get/put/delete の交錯、容量がゼロまたは負の場合(ValueError を送出)を含む。 単一の Python モジュールとして完全な実装を提供してください。並行性戦略とそれが整合性を保つ理由の簡潔な説明を付けてください。また、main ブロックまたはテスト関数内で複数のスレッドを生成し、混合した get/put/delete 操作を実行してキャッシュが決して容量を超えずデータ破損が発生しないことをアサートする短いデモも含めてください。

補足情報

このタスクは、データ構造設計(O(1) の LRU 操作のための双方向連結リストとハッシュマップの組み合わせ)と細粒度の並行制御を組み合わせる能力をテストします。強い解答は、単純な単一ロック方式を超えて競合を低減するために、ストライプまたはセグメント化ロック、リード・ライトロック、またはロックフリーのデータ構造のような手法を使用しつつ、厳密な LRU 意味論とスレッド安全性を保証します。

採点方針

高品質な解答は次の次元で評価されるべきです。まず、正確性:LRU の追い出しロジックは厳密に正しくなければならない。get は使用順序を更新し、put は容量に達したときに真の最少最近使用項目を追い出し、delete は正しく動作すること。次に、並行性設計:解決策は単一のグローバルロックを超え、セグメント化ロック、リード・ライトロック、個々のノードロックなどの有意義な戦略を用いて競合を低減しつつスレッド安全性を維持しているべきである。並行性戦略の説明は明確かつ正確であること。第三に、エッジケースの扱い:容量が1、容量がゼロまたは負の場合に ValueError を送出すること、同一キーに対する同時操作の扱いなどが適切に処理されていること。第四に、デモまたはテストコード:実際に複数スレッドを生成し、混合操作を実行し、容量制限とデータ整合性に関する有意義なアサーションを含んでいること。第五に、コード品質:実装はきれいで構造化され、適切な Python の慣用を用いていること。単一のグローバルロックを使っているが他は正しい解答には部分点を与えることが適切だが、本来は真の細粒度並行性を有する解答の評価は高くなるべきである。競合状態、誤った追い出し順序、またはエッジケース処理の欠落がある解答は大幅に低く評価されるべきである。

回答

import threading
import time
from collections import OrderedDict

class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        if capacity <= 0:
            raise ValueError("容量は正の整数である必要があります。")
        self.capacity = capacity
        self.cache = {}
        self.head = Node(None, None)  # ダミーヘッド
        self.tail = Node(None, None)  # ダミーテール
        self.head.next = self.tail
        self.tail.prev = self.head
        self...
全文を表示
import threading
import time
from collections import OrderedDict

class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None

class LRUCache:
    def __init__(self, capacity: int):
        if capacity <= 0:
            raise ValueError("容量は正の整数である必要があります。")
        self.capacity = capacity
        self.cache = {}
        self.head = Node(None, None)  # ダミーヘッド
        self.tail = Node(None, None)  # ダミーテール
        self.head.next = self.tail
        self.tail.prev = self.head
        self.lock = threading.RLock() # この例では簡潔さのためにRLockを使用していますが、よりきめ細かなアプローチも可能です。

    def _remove_node(self, node):
        prev_node = node.prev
        next_node = node.next
        prev_node.next = next_node
        next_node.prev = prev_node

    def _add_to_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        with self.lock:
            if key in self.cache:
                node = self.cache[key]
                self._remove_node(node)
                self._add_to_front(node)
                return node.value
            return None

    def put(self, key, value):
        with self.lock:
            if key in self.cache:
                node = self.cache[key]
                node.value = value
                self._remove_node(node)
                self._add_to_front(node)
            else:
                if len(self.cache) >= self.capacity:
                    # LRUアイテムを削除
                    lru_node = self.tail.prev
                    self._remove_node(lru_node)
                    del self.cache[lru_node.key]
                new_node = Node(key, value)
                self.cache[key] = new_node
                self._add_to_front(new_node)

    def delete(self, key):
        with self.lock:
            if key in self.cache:
                node = self.cache[key]
                self._remove_node(node)
                del self.cache[key]
                return True
            return False

    def __len__(self):
        with self.lock:
            return len(self.cache)

    def __str__(self):
        with self.lock:
            items = []
            current = self.head.next
            while current != self.tail:
                items.append(f"({current.key}: {current.value})")
                current = current.next
            return " -> ".join(items)

# --- 並行処理戦略の説明 ---
# この実装では、すべての操作(get、put、delete)に単一のthreading.RLockを使用しています。
# これによりスレッドセーフティは保証されますが、プロンプトで要求されているように競合を最小限に抑えることはできません。
# より高度で、きめ細かなアプローチとしては、キャッシュをセグメント化したり、
# ロックフリーデータ構造を使用したりすることが考えられます。例えば、キャッシュエントリのサブセットを保護する複数のロック(ストライピングロック)を使用したり、
# 読み取り/書き込みロックを使用して複数のリーダーがキャッシュの異なる部分に同時にアクセスできるようにしたりすることができます。
# 現在のRLockは、一度に1つのスレッドのみがキャッシュの内部状態(辞書と双方向連結リスト)を変更できることを保証します。
# これにより、複数のスレッドが同じノードを更新しようとしたり、ノードを追加/削除したり、エントリを同時に削除したりして、データ破損やLRU順序の間違いにつながる競合状態を防ぎます。
# RLock(再入可能ロック)の使用は、スレッドがロックを複数回取得できるため、複雑なシナリオで役立つことがありますが、この特定の用途では標準的なLockと比較して競合特性を根本的に変更するものではありません。

# --- デモンストレーション ---

def worker(cache, operations, thread_id):
    for op, key, value in operations:
        if op == 'get':
            cache.get(key)
        elif op == 'put':
            cache.put(key, value)
        elif op == 'delete':
            cache.delete(key)
        # いくらかの作業をシミュレートし、他のスレッドが実行できるようにする
        time.sleep(0.001)

def run_demonstration():
    print("LRUキャッシュデモンストレーションを実行中...")
    capacity = 5
    cache = LRUCache(capacity)
    num_threads = 10
    ops_per_thread = 50

    all_operations = []
    for i in range(num_threads):
        thread_ops = []
        for j in range(ops_per_thread):
            op_type = ['get', 'put', 'delete'][j % 3] # 操作タイプをローテーション
            key = (i * ops_per_thread + j) % (capacity * 2) # キャッシュにあるかもしれないし、ないかもしれないキー
            value = f"val_{i}_{j}"
            thread_ops.append((op_type, key, value))
        all_operations.append(thread_ops)

    threads = []
    for i in range(num_threads):
        thread = threading.Thread(target=worker, args=(cache, all_operations[i], i))
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    print(f"デモンストレーション完了。最終キャッシュサイズ: {len(cache)}")
    # キャッシュが容量を超えないことをアサートする
    assert len(cache) <= capacity, f"キャッシュ容量を超過しました!現在のサイズ: {len(cache)}, 容量: {capacity}"
    print("アサート成功:キャッシュ容量は維持されています。")
    # 基本的な整合性チェック
    print("基本的な整合性チェックを実行中...")
    # これは非常に基本的なチェックです。より堅牢なテストでは、期待される状態を追跡する必要があります。
    # とりあえず、サイズが負にならない、アクセス中に例外が発生しないことを確認します。
    try:
        # 投入された可能性が高いキーをいくつかアクセスしてみる
        for i in range(min(5, capacity)):
            key_to_check = (i * ops_per_thread) % (capacity * 2)
            cache.get(key_to_check)
        print("基本的な整合性チェックに成功しました。")
    except Exception as e:
        print(f"整合性チェックが例外で失敗しました: {e}")
        assert False, "キャッシュ整合性チェックに失敗しました。"

if __name__ == "__main__":
    run_demonstration()

判定

2位

勝利票

0 / 3

平均スコア

63
採点モデル OpenAI GPT-5.4

総合点

62

総評

回答Aは、辞書と双方向連結リストを用いた従来のLRUキャッシュを実装しており、基本的なシングルスレッドLRUロジックは妥当です。正の容量検証、get/put/deleteを正しく処理し、マルチスレッドのデモも含まれています。しかし、すべての操作に対して1つのグローバルRLockを明示的に使用しており、これはプロンプトで要求されている粗粒度ロックの回避に違反し、異なるキーに対する同時getを許可しません。デモのアサーションは弱く、基本的な容量チェック以上の厳密な同時実行性の正しさやデータ整合性を意味のある形で検証していません。

採点詳細を表示

正確さ

重み 35%
65

辞書と双方向連結リストの実装は標準的なLRU動作に対しては正しいです。getは新しさを更新し、putはtail.prevを追い出します。削除動作も正しいです。しかし、意図された同時実行設定に対する正しさは、単一ロックによる完全な直列化によって制限されており、タスクが強調するより困難な競合状態に対処していません。

完全性

重み 20%
63

必要なAPI、非正の容量に対する検証、説明、マルチスレッドデモが含まれています。しかし、競合の最小化や異なるキーに対する同時ノンブロッキング読み取りの要求を満たしておらず、デモではインターリーブ下での正しさを深くテストしていません。

コード品質

重み 20%
70

コードは読みやすく、わかりやすく、明確に整理されています。リスト操作用のヘルパーメソッドも合理的です。しかし、不要なOrderedDictのインポートがあり、設計が要求されたものではないことを説明で認めており、テストロジックはかなり表面的です。

実用性

重み 15%
56

単純なスレッドセーフLRUキャッシュとしては実用的ですが、単一のグローバルロックは高い競合を引き起こし、スケーラブルな同時アクセスという主な実用的な要件を損ないます。デモは、実際のマルチスレッドワークロードに対する信頼性を限定的にしか与えません。

指示遵守

重み 10%
45

APIとモジュール形式の要件は満たしていますが、粗粒度のグローバルロックの回避と、異なるキーに対する同時getの許可という指示を明らかに満たしていません。これは中心的な要件に対する大きな欠落です。

総合点

61

総評

回答Aは、適切な双方向連結リストとハッシュマップ構造、正しい追い出しロジック、および適切なエッジケース処理を備えた、正しいLRUキャッシュ実装を提供します。コードはクリーンで読みやすいです。しかし、すべての操作に単一のRLockを使用しているため、コアとなる並行性の要件を根本的に満たしていません。これは、作成者自身もプロンプトで許容されないと認めている点です。デモンストレーションは存在しますが、アサーションは最小限です。これは、細かい並行性の要件を満たすには大幅な再設計が必要な、堅実なベースライン実装です。

採点詳細を表示

正確さ

重み 35%
65

回答Aは、適切な双方向連結リストとハッシュマップを備えた正しいLRUキャッシュを実装しています。追い出しロジック、get/put/delete操作、およびエッジケース処理(容量が正でない場合のValueError)はすべて正しいです。しかし、すべてに単一のRLockを使用しており、作成者自身もこれが細かい並行性の要件を満たさないことを認めています。LRUセマンティクスは正しいですが、並行性モデルは単純すぎます。

完全性

重み 20%
60

回答Aには、必要なすべての操作、エッジケース処理、および複数のスレッドを使用したデモンストレーションが含まれています。しかし、コア要件である細かい並行性の実装に明示的に失敗しています(単一のグローバルRLockを使用)。デモンストレーションは存在しますが、アサーションは最小限であり、整合性チェックは表面的です。

コード品質

重み 20%
65

回答Aは、優れた構造を持ち、クリーンで読みやすいです。ダミーのヘッド/テールセンチネルの使用は適切です。しかし、ここでRLockを使用するのは不要であり、コメントでは実装が指定された要件を満たしていないことが認識されています。コードはよく整理されていますが、並行性の設計は既知の欠点です。

実用性

重み 15%
50

回答Aの単一ロックアプローチは、高並行性シナリオでの実用的な価値が限られています。正しく機能しますが、負荷下ではボトルネックになります。作成者はこの制限を認識しています。本番環境での使用には、実装の大幅な再設計が必要になります。

指示遵守

重み 10%
55

回答Aはほとんどの指示に従っていますが、単一のグローバルロックを超えるというコア要件に明示的に失敗しています。プロンプトには「すべてを囲む単一の粗粒度ロックは許容されない」と記載されており、回答Aはまさにそれを使用しています。作成者はコメントでこれを認めていますが、問題は解決されていません。デモンストレーションとエッジケース処理は存在します。

採点モデル Google Gemini 2.5 Pro

総合点

66

総評

回答Aは、標準的な辞書と双方向連結リストを使用したLRUキャッシュのスレッドセーフな実装を提供しており、コードはクリーンで理解しやすいです。しかし、すべての操作に単一のグローバルロックを使用しているため、プロンプトのコアとなる並行処理要件を根本的に満たしていません。この制限を説明で正しく認識しているものの、この設計上の選択は、要求されたものよりもはるかに単純で準拠していないソリューションとなっています。デモコードは機能しますが、その整合性チェックは非常に基本的なものです。

採点詳細を表示

正確さ

重み 35%
70

単一のグローバルロックにより、LRUロジックとスレッドセーフティは正しいです。しかし、すべての操作をシリアル化し、パフォーマンス目標を満たせないため、要求された*並行*設計の正しい実装ではありません。

完全性

重み 20%
75

回答は、クラス実装、説明、デモという要求されたすべてのコンポーネントを提供しています。しかし、コアとなる実装は、プロンプトの並行処理要件に関しては不完全です。

コード品質

重み 20%
75

コードはクリーンで、コメントも適切であり、標準的なPythonのイディオムに従っています。辞書と双方向連結リストの実装は、直接的かつ正確です。

実用性

重み 15%
50

単一ロックのLRUキャッシュは、ロックがすぐにボトルネックになるため、高並行処理アプリケーションでの実用的な価値は限定的です。基本的な教科書的な例としては役立ちますが、高性能なソリューションではありません。

指示遵守

重み 10%
40

回答は、プロンプトの重要なネガティブ制約「すべてを囲む単一の粗粒度ロックは許容されない」に従っていません。すべての操作に単一のRLockを使用することで、タスクの難しさの中心であったこの指示に直接違反しています。

"""競合を最小化した並行 LRU キャッシュ(厳密な LRU)。

並行性戦略(概要):

  • キャッシュは、key->Node のために dict を使用し、グローバルな LRU 順序のために双方向リンクリストを使用します。
  • キーと値の操作における競合を減らすために、キー空間は複数のロックにまたがってストライプ化されています。
    特定のキーに対する更新(マッピングおよびノードフィールドの挿入/更新/削除/読み取り)は、
    そのキーのストライプロックのみを使用します。
  • 厳密なグローバル LRU セマンティクス を保つために、LRU リストのすべての変更は
    専用の lru_lock によって直列化されます。これにより、リストを破損させうる競合を回避し、
    「touch」操作に単...
全文を表示

"""競合を最小化した並行 LRU キャッシュ(厳密な LRU)。

並行性戦略(概要):

  • キャッシュは、key->Node のために dict を使用し、グローバルな LRU 順序のために双方向リンクリストを使用します。
  • キーと値の操作における競合を減らすために、キー空間は複数のロックにまたがってストライプ化されています。
    特定のキーに対する更新(マッピングおよびノードフィールドの挿入/更新/削除/読み取り)は、
    そのキーのストライプロックのみを使用します。
  • 厳密なグローバル LRU セマンティクス を保つために、LRU リストのすべての変更は
    専用の lru_lock によって直列化されます。これにより、リストを破損させうる競合を回避し、
    「touch」操作に単一の全順序が存在することを保証します。
  • エビクションには協調が必要です: lru_lock の下で現在の末尾(最も最近使われていないもの)を選択し、
    次にそのキーのストライプロックを取得して、それが依然として同じノードであることを確認してから
    マップから削除します。これにより、並行更新されたキーを誤ってエビクトすることを防ぎます。

これが正しい理由:

  • LRU リストは lru_lock の下でのみ変更されるため、破損することがなく、各 touch
    (既存キーに対する get/put)は完全に順序付けられ、厳密な LRU が実現されます。
  • マップエントリとノードの値はそのキー用のストライプロックによって保護されるため、同じキーに対する
    並行した更新/削除によって状態が破損することを防ぎます。
  • エビクションは、lru_lock の下でエビクションが決定された瞬間の実際の末尾に基づきます。候補ノードが
    並行した touch/delete によって変化した場合、エビクションは再試行されます。

注: Python は複雑な共有構造に対する真のロックフリープリミティブを提供していません。この
実装では、関心事を分離することにより、すべて を単一のグローバルロックで囲むことを避けています:

  • キーレベルの操作はストライプ化されています。
  • リスト順序の維持のみに単一のロックを使用します。

デモは main に含まれています。
"""

from future import annotations

import threading
import time
import random
from dataclasses import dataclass
from typing import Any, Optional, Dict, List

@dataclass
class _Node:
key: Any
value: Any
prev: Optional["_Node"] = None
next: Optional["_Node"] = None
alive: bool = True # 削除/エビクト時に False に設定される

class ConcurrentLRUCache:
def init(self, capacity: int, stripes: int = 64):
if capacity <= 0:
raise ValueError("capacity は正の値でなければなりません")
if stripes <= 0:
raise ValueError("stripes は正の値でなければなりません")
self.capacity = int(capacity)
self._map: Dict[Any, _Node] = {}

    # 双方向リンクリストの番兵 head/tail(MRU は head.next、LRU は tail.prev)
    self._head = _Node("<HEAD>", None)
    self._tail = _Node("<TAIL>", None)
    self._head.next = self._tail
    self._tail.prev = self._head

    # リスト操作とサイズ/エビクション判定を直列化する
    self._lru_lock = threading.Lock()

    # キーレベルの map/node 操作用ストライプロック
    self._stripes_n = int(stripes)
    self._stripes: List[threading.Lock] = [threading.Lock() for _ in range(self._stripes_n)]

    # サイズは lru_lock の下で追跡する
    self._size = 0

def _stripe_lock(self, key: Any) -> threading.Lock:
    return self._stripes[hash(key) % self._stripes_n]

# ----- 双方向リンクリストのヘルパー(self._lru_lock を保持していることが必要) -----
def _unlink(self, node: _Node) -> None:
    p = node.prev
    n = node.next
    if p is not None:
        p.next = n
    if n is not None:
        n.prev = p
    node.prev = None
    node.next = None

def _link_front(self, node: _Node) -> None:
    first = self._head.next
    node.prev = self._head
    node.next = first
    self._head.next = node
    if first is not None:
        first.prev = node

def _move_to_front(self, node: _Node) -> None:
    # node はリスト内に存在していなければならない
    self._unlink(node)
    self._link_front(node)

def _current_lru_node(self) -> Optional[_Node]:
    # 実際の LRU ノード(tail.prev)を返す。空なら None
    n = self._tail.prev
    if n is None or n is self._head:
        return None
    return n

# ----- 公開 API -----
def get(self, key: Any) -> Any:
    lock = self._stripe_lock(key)
    with lock:
        node = self._map.get(key)
        if node is None or not node.alive:
            return None
        value = node.value
    # 先頭への移動にはリストロックが必要。競合を減らすため、ストライプロックの外で行う。
    with self._lru_lock:
        # 削除/置換されたノードを移動しないよう、ストライプロック下で所属を再確認する。
        with lock:
            cur = self._map.get(key)
            if cur is not node or cur is None or not cur.alive:
                return value  # 読み取り時点では value は有効だった。競合により touch は失われた。
        # lru_lock の下では、このノードを安全に移動できる。
        self._move_to_front(node)
    return value

def put(self, key: Any, value: Any) -> None:
    lock = self._stripe_lock(key)
    # まず、キーが存在するかを確認し、ストライプロック下で更新する。
    with lock:
        node = self._map.get(key)
        if node is not None and node.alive:
            node.value = value
            # lru_lock の下で touch(先頭へ移動)。
            # ストライプロックは素早く解放する? 短く保てる: 更新は完了済み。
            pass
        else:
            node = None

    if node is not None:
        with self._lru_lock:
            # 依然として存在し、同じノードであることを保証する
            with lock:
                cur = self._map.get(key)
                if cur is node and cur is not None and cur.alive:
                    self._move_to_front(node)
        return

    # 新しいノードを挿入
    new_node = _Node(key, value)
    with self._lru_lock:
        # 一貫した挿入+リンクを保証するため、lru_lock を保持したままストライプロックを取得する。
        with lock:
            existing = self._map.get(key)
            if existing is not None and existing.alive:
                # 誰かが並行して挿入した。その代わりにそれを更新する。
                existing.value = value
                # touch する
                self._move_to_front(existing)
                return
            # マッピングを挿入してリンクする
            self._map[key] = new_node
            self._link_front(new_node)
            self._size += 1

        # 容量超過中はエビクトする
        while self._size > self.capacity:
            if not self._evict_one_locked():
                # 競合によりエビクションに失敗した場合は再試行する
                continue

def _evict_one_locked(self) -> bool:
    """単一の LRU ノードをエビクトする。
    self._lru_lock を保持していることが必要。
    エビクションが行われた場合は True、再試行が必要な場合は False を返す。
    """
    victim = self._current_lru_node()
    if victim is None:
        return True
    vkey = victim.key
    vlock = self._stripe_lock(vkey)
    with vlock:
        cur = self._map.get(vkey)
        if cur is not victim or cur is None or not cur.alive:
            # victim が変わった、またはすでに削除されている。再試行。
            return False
        # まず map から削除する
        del self._map[vkey]
        victim.alive = False
        # 次にリストから削除する(lru_lock は保持している)
        self._unlink(victim)
        self._size -= 1
    return True

def delete(self, key: Any) -> bool:
    lock = self._stripe_lock(key)
    # エビクション/挿入経路とのデッドロックを避けるため、先に lru_lock を取得する。
    with self._lru_lock:
        with lock:
            node = self._map.get(key)
            if node is None or not node.alive:
                return False
            del self._map[key]
            node.alive = False
            # LRU リストから切り離す
            self._unlink(node)
            self._size -= 1
            return True

def __len__(self) -> int:
    with self._lru_lock:
        return self._size

def _assert_invariants(self) -> None:
    """デバッグ/検査ルーチン: 内部整合性を検証する。"""
    with self._lru_lock:
        # リスト構造を確認し、ノード数を数える
        count = 0
        seen = set()
        n = self._head.next
        prev = self._head
        while n is not None and n is not self._tail:
            assert n.prev is prev
            assert n.alive
            assert n.key not in seen
            seen.add(n.key)
            count += 1
            prev = n
            n = n.next
        assert n is self._tail
        assert self._tail.prev is prev
        assert count == self._size
        # map とリストが一致していること(ベストエフォート。完全にするにはストライプロックが必要)。
        # lru_lock の下でも、map はストライプロック下で変更されうるため、保守的な検査のみを行う:
        # すべてのリストノードは map に存在していなければならない。
        for k in seen:
            lock = self._stripe_lock(k)
            with lock:
                m = self._map.get(k)
                assert m is not None and m.alive and m.key == k

def _demo() -> None:
cap = 32
cache = ConcurrentLRUCache(capacity=cap, stripes=32)

stop = threading.Event()
errors = []

keys = [f"k{i}" for i in range(64)]

def worker(tid: int) -> None:
    rnd = random.Random(12345 + tid)
    try:
        while not stop.is_set():
            op = rnd.random()
            k = rnd.choice(keys)
            if op < 0.50:
                cache.get(k)
            elif op < 0.85:
                cache.put(k, (tid, rnd.randint(0, 10_000)))
            else:
                cache.delete(k)

            # ときどきサイズが容量を超えていないことを確認する
            if rnd.random() < 0.01:
                assert len(cache) <= cap
    except Exception as e:
        errors.append(e)
        stop.set()

threads = [threading.Thread(target=worker, args=(i,), daemon=True) for i in range(12)]
for t in threads:
    t.start()

# 少し実行する
time.sleep(1.5)

# 停止して join する
stop.set()
for t in threads:
    t.join(timeout=2.0)

# ワーカーエラーなし
if errors:
    raise errors[0]

# 最終的な不変条件の検査
assert len(cache) <= cap
cache._assert_invariants()

# 基本的な健全性: 複数スレッドから同じキーに対して put/get を繰り返しても破損しないはず
cache.put("sentinel", 1)
assert cache.get("sentinel") == 1
assert cache.delete("sentinel") is True
assert cache.get("sentinel") is None

if name == "main":
_demo()
print("デモ完了; キャッシュの不変条件は保たれています。")

判定

1位 | 勝者

勝利票

3 / 3

平均スコア

87
採点モデル OpenAI GPT-5.4

総合点

82

総評

回答Bは、ストライプ化されたキーごとのロックと専用のLRUリストロックを組み合わせた、大幅に強力な同時実行設計を提供しており、要求されたきめ細かいアプローチに非常に近いです。実装は完了しており、容量の検証、get/put/deleteのサポート、エビクションの調整、不変条件チェック付きのスレッドデモが含まれています。同時実行戦略の説明は明確で、技術的に根拠があります。1つの注意点として、レース条件により更新が失われた場合でもgetは値を返す可能性があるため、厳密なLRUという主張は、非常に高い同時実行のインターリーブ下ではいくらか議論の余地がありますが、全体としては回答Aよりもタスクにずっと沿っています。

採点詳細を表示

正確さ

重み 35%
79

実装は、エビクションとリストの変更のための明示的な調整を備え、同時アクセス向けに慎重に設計されています。内部の一貫性をよく維持し、不変条件チェックが含まれています。主な弱点は、同時削除または置換後に更新がドロップされた場合でもgetが値を返す可能性があることで、レース下での厳密なLRUセマンティクスの主張を弱めますが、全体としてコアの動作は強力です。

完全性

重み 20%
84

要求された操作、容量検証、同時実行の説明、および複数のスレッドが混合操作を実行するデモを含む完全なモジュールを提供します。また、内部不変条件チェックや、削除や繰り返しアクセスなどのエッジケースのカバレッジも含まれています。これは、プロンプトに対してほぼ完全に完了しています。

コード品質

重み 20%
85

コードは構造化されており、型ヒントとデータクラスを適切に使用し、リストヘルパーをパブリックAPIから分離し、ロックの期待値を明確に文書化しています。同時実行設計は、読みやすさを保ちながら、より洗練されています。わずかな複雑さと微妙なレース処理のトレードオフが、さらに高いスコアを妨げています。

実用性

重み 15%
81

ストライプ化されたロックで競合を減らし、リスト順序ロジックのみを集中化するため、実際の同時実行ワークロードに対してはるかに実用的です。デモと不変条件チェックにより、信頼性が向上します。Pythonのスレッドとリスト順序のためのグローバルLRUロックによって依然として制約されていますが、意味のある有用性があります。

指示遵守

重み 10%
88

プロンプトに密接に従っています:固定容量キャッシュ、要求されたメソッド、無効な容量に対するValueError、同時実行戦略の説明、および混合操作とアサーションを含むスレッドデモ。設計は、Aよりも忠実に、競合の最小化と厳密なLRUの維持を直接ターゲットにしています。

総合点

83

総評

回答Bは、キーレベルの操作のためのストライピングロックとリスト順序維持のための専用lru_lockを備えた、真にきめ細かな同時実行LRUキャッシュを提供します。実装は、同時操作、競合状態(リトライとaliveフラグによる)、およびすべてのエッジケースを正しく処理します。並行戦略の説明は明確かつ正確です。デモンストレーションは、複数のスレッド、意味のあるアサーション、および不変条件チェッカーを備えており、堅牢です。コード品質は高く、型ヒント、データクラス、および関心の明確な分離がうまく利用されています。get()における軽微な理論的な競合状態は、両方のロック下での再チェックによって軽減されます。

採点詳細を表示

正確さ

重み 35%
85

回答Bは、ストライピングロックと専用lru_lockを備えた正しいLRUセマンティクスを実装しています。追い出しロジックは、競合状態のリトライを処理し、aliveフラグは古いノードの問題を防ぎ、get/put/delete操作は慎重に調整されています。_assert_invariantsメソッドは、追加の正しさ検証を提供します。get()には、ストライプロックがlru_lockを取得する前に解放されるという微妙な問題があり、理論的には競合を許容する可能性がありますが、両方のロック下での再チェックによってこれが軽減されます。全体的な正しさは強力です。

完全性

重み 20%
85

回答Bは非常に包括的です。すべての必須操作を実装し、エッジケース(容量とストライプが正でない場合のValueError)、専用lru_lockを備えたストライピングロックの使用、_assert_invariantsメソッドの包含、および意味のあるアサーションを備えた堅牢なマルチスレッドデモンストレーションを提供します。並行戦略の説明は詳細かつ正確です。

コード品質

重み 20%
80

回答Bは、関心の明確な分離を備えた、よく構造化されています。_Nodeのためのデータクラス、型ヒント、およびdocstringの使用は、可読性を向上させます。_evict_one_lockedヘルパーと_assert_invariantsメソッドは、優れた設計を示しています。aliveフラグパターンは、同時削除/追い出しを処理するためのクリーンな方法です。ロック順序(lru_lockがストライプロックより前)は、デッドロックを防ぐために一貫して維持されています。

実用性

重み 15%
75

回答Bは、そのストライピングロックアプローチにより、真の実用的な価値があります。デフォルトの64ストライプは、キーレベルの操作における競合を大幅に削減します。リスト順序維持(lru_lock)とキーレベル操作(ストライプロック)の分離は、同時ワークロードでうまく機能する堅実な設計です。設定可能なストライプパラメータは柔軟性を加えています。

指示遵守

重み 10%
85

回答Bはすべての指示に従っています。ストライプロックによるきめ細かな並行処理を実装し、並行戦略の明確な説明を提供し、すべてのエッジケースを処理し、アサーションを備えたマルチスレッドデモンストレーションを含み、無効な容量に対してValueErrorを発生させます。実装は、要求された単一のグローバルロックを実際に超えています。

採点モデル Google Gemini 2.5 Pro

総合点

95

総評

回答Bは、洗練された高品質なソリューションを提供し、細粒度ロックを備えた同時実行LRUキャッシュを正しく実装しています。キーと値の操作にはストライプ化されたロッキングメカニズムを、リストの操作には別のロックを使用して厳密なLRU順序を維持しており、競合を最小限に抑えるというプロンプトの要件に直接対応しています。コードはよく構造化されており、複雑な同時実行戦略の説明は明確かつ正確であり、デモンストレーションには堅牢な不変条件チェックメソッドが含まれています。この回答は、データ構造と同時実行プログラミングの課題の両方に対する深い理解を示しています。

採点詳細を表示

正確さ

重み 35%
95

実装はLRUロジックが正しいだけでなく、細粒度ロッキング戦略に固有の複雑な競合状態も正しく処理しているようです。ストライプロックとLRUロック間の調整により厳密なLRU順序を維持するロジックは健全であり、うまく実装されています。

完全性

重み 20%
100

この回答は完全に網羅されています。すべての機能要件と同時実行要件を満たす完全に機能するクラス、その高度な戦略に関する詳細かつ正確な説明、そして強力な整合性チェックを備えた堅牢なマルチスレッドデモンストレーションを提供します。

コード品質

重み 20%
90

コード品質は優れています。明確なヘルパーメソッドでよく構造化されており、ノードにはデータクラスを使用し、デバッグと検証のための貴重な`_assert_invariants`メソッドが含まれています。コードは複雑ですが、優れた構成とコメントにより読みやすさを保っています。

実用性

重み 15%
90

この実装は高い実用価値を持っています。ストライプ化されたロッキングを備えた同時実行LRUキャッシュは、高性能なマルチスレッドシステムを構築するための非常に有用なデータ構造です。この設計は、実際の problemas に直接適用可能です。

指示遵守

重み 10%
100

回答はすべての指示に完全に準拠しています。必要なメソッドを実装し、エッジケースを処理し、説明とデモを提供し、そして最も重要なことに、洗練された細粒度の同時実行モデルを実装することにより、単一のグローバルロックを回避するという制約を遵守しています。

比較結果サマリー

最終順位は、採点者ごとの順位集約(平均順位 + ボルダ方式の同点処理)で決定します。平均点は参考表示です。

採点者数: 3

勝利票

3 / 3

平均点

87
この回答を見る

採点結果

採点モデル Google Gemini 2.5 Pro

勝者理由

回答Bが明確な勝者です。なぜなら、タスクの主な課題である、競合を最小限に抑えるための細粒度ロッキング戦略を備えた同時実行LRUキャッシュの実装に成功しているからです。回答Aは、プロンプトで明示的に禁止されている単一のグローバルロックを使用しているため、この重要な要件を満たせていません。回答Bのストライプロッキングと専用リストロックの組み合わせは、はるかに高度で実用的なソリューションであり、並行プログラミングに関する優れた知識を示しています。さらに、回答Bのコード品質、説明、およびテストはすべて、より徹底的で堅牢です。

勝者理由

回答Bは、このタスクの中心的な要件である、要求されたきめ細かい並行処理戦略(ストライピングロック)を実際に実装しているため、圧倒的に優れています。回答Aは、明示的に単一のグローバルロックを使用しており、これがプロンプトの要件に違反していることを認めています。並行処理設計以外にも、回答Bはより堅牢なデモンストレーション、優れた不変条件チェック、および全体的に高いコード品質を備えています。タスクでは、単一の粗粒度ロックは許容されないと specifically 述べられているため、回答Aのアプローチはコア要件の根本的な失敗となります。

採点モデル OpenAI GPT-5.4

勝者理由

回答Bが勝利したのは、タスクの核心的な課題である、競合を削減しつつスレッドセーフティとLRU動作を維持するという点を意味のある形で解決しているためです。回答Bは、単一のグローバルロックの代わりに、ストライプ化されたロックと個別のLRUロックを使用しており、明確な正当性に関する考察が含まれ、エビクションレースをより慎重に処理し、不変条件の表明を用いたより強力な並行テストカバレッジを提供しています。回答Aはよりシンプルで機能的にはほぼ正しいですが、単一の粗粒度ロックはプロンプトの主要な要件に直接失敗します。

X f L