05-lru-cache: implement mutex-guarded LRU over container/list

map[string]*list.Element + doubly linked list; Get/Put move hits to the
front, Put evicts from the back when at capacity, and every method
(including Len) takes the mutex.

Add TestLenRacesWithPut, which calls Len concurrently with a writer —
the existing test only read Len after wg.Wait(), so an unsynchronized
Len would never trip the race detector there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MpK9niZm4jcs3mAJbxMfi
This commit is contained in:
2026-09-02 09:34:08 +02:00
co-authored by Claude Fable 5
parent 86a61da36f
commit b290957fc9
2 changed files with 90 additions and 5 deletions
+48 -3
View File
@@ -16,35 +16,80 @@
// Run the tests: go test -race ./05-lru-cache/ // Run the tests: go test -race ./05-lru-cache/
package main package main
import "fmt" import (
"container/list"
"fmt"
"sync"
)
type entry struct {
key string
val any
}
type LRU struct { type LRU struct {
// TODO: fields (mutex, capacity, map[string]*list.Element, *list.List) // TODO: fields (mutex, capacity, map[string]*list.Element, *list.List)
// Remember: the list element must store the KEY too, or you can't // Remember: the list element must store the KEY too, or you can't
// delete the map entry on eviction. // delete the map entry on eviction.
mu sync.Mutex
capacity int
lruList *list.List
kvMap map[string]*list.Element
} }
// NewLRU creates an LRU cache with the given capacity (capacity >= 1). // NewLRU creates an LRU cache with the given capacity (capacity >= 1).
// TODO: implement me. // TODO: implement me.
func NewLRU(capacity int) *LRU { func NewLRU(capacity int) *LRU {
return &LRU{} return &LRU{capacity: capacity, lruList: list.New(), kvMap: make(map[string]*list.Element)}
} }
// Get returns the value and whether it was present. Hit = mark MRU. // Get returns the value and whether it was present. Hit = mark MRU.
// TODO: implement me. // TODO: implement me.
func (c *LRU) Get(key string) (any, bool) { func (c *LRU) Get(key string) (any, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if el, ok := c.kvMap[key]; ok {
c.lruList.MoveToFront(el)
return el.Value.(*entry).val, true
}
return nil, false return nil, false
} }
// Put inserts/updates a value, evicting the LRU entry if over capacity. // Put inserts/updates a value, evicting the LRU entry if over capacity.
// TODO: implement me. // TODO: implement me.
func (c *LRU) Put(key string, val any) { func (c *LRU) Put(key string, val any) {
// lock mutex
c.mu.Lock()
defer c.mu.Unlock()
// if key exists, update value and move it to the front of the list and return/quit
if el, ok := c.kvMap[key]; ok {
c.lruList.MoveToFront(el)
el.Value.(*entry).val = val
return
}
// if len > capacity, remove the last element from both the list and the map
if c.lruList.Len() >= c.capacity {
// remove last element from list
lastEl := c.lruList.Back()
c.lruList.Remove(lastEl)
// remove it by key from map
delete(c.kvMap, lastEl.Value.(*entry).key)
}
// if key doesn't exist, insert'
el := c.lruList.PushFront(&entry{key: key, val: val})
c.kvMap[key] = el
} }
// Len returns the current number of entries. // Len returns the current number of entries.
// TODO: implement me. // TODO: implement me.
func (c *LRU) Len() int { func (c *LRU) Len() int {
return 0 c.mu.Lock()
defer c.mu.Unlock()
return c.lruList.Len()
} }
func main() { func main() {
+40
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"sync" "sync"
"testing" "testing"
"time"
) )
func TestBasicPutGet(t *testing.T) { func TestBasicPutGet(t *testing.T) {
@@ -82,3 +83,42 @@ func TestConcurrentAccess(t *testing.T) {
t.Fatalf("capacity exceeded under concurrency: %d", c.Len()) t.Fatalf("capacity exceeded under concurrency: %d", c.Len())
} }
} }
// TestLenRacesWithPut calls Len concurrently WITH writers, not after them.
// TestConcurrentAccess only reads Len after wg.Wait(), which establishes
// happens-before with every writer — so an unsynchronized Len can't race
// there. The race detector reports only races that actually occur.
func TestLenRacesWithPut(t *testing.T) {
c := NewLRU(64)
done := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
go func() { // writer
defer wg.Done()
for i := 0; ; i++ {
select {
case <-done:
return
default:
c.Put(fmt.Sprintf("k%d", i%128), i)
}
}
}()
wg.Add(1)
go func() { // reader, concurrently
defer wg.Done()
for i := 0; i < 10000; i++ {
if n := c.Len(); n > 64 {
t.Errorf("Len observed %d > capacity 64 (mid-Put state leaked)", n)
return
}
}
}()
go func() { wg.Wait() }()
time.Sleep(50 * time.Millisecond)
close(done)
wg.Wait()
}