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:
+48
-3
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user