Skip to content

Commit

Permalink
WIP: Sample concurrency solutions
Browse files Browse the repository at this point in the history
There are more to come (computing, coarse-grained generations), as well as a lot of commentary
  • Loading branch information
illicitonion committed Mar 20, 2024
1 parent 28f53c1 commit 99be1dd
Show file tree
Hide file tree
Showing 9 changed files with 520 additions and 0 deletions.
24 changes: 24 additions & 0 deletions projects/concurrency/atomics/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"fmt"
"sync"
"sync/atomic"
)

var x atomic.Int32

func increment(wg *sync.WaitGroup) {
x.Add(1)
wg.Done()
}

func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x.Load())
}
156 changes: 156 additions & 0 deletions projects/concurrency/lru_cache_everything_in_buckets/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package cache

import (
"sync"
"time"
)

type Cache[K comparable, V any] struct {
entryLimit int

mu sync.Mutex
entries map[K]*cacheEntry[V]
unsuccessfulReads uint64
evictedSuccessfulReads uint64
evicted uint64
evictedNeverRead uint64
}

type cacheEntry[V any] struct {
value V
lastAccess time.Time
reads uint64
}

func NewCache[K comparable, V any](entryLimit int) *Cache[K, V] {
return &Cache[K, V]{
entryLimit: entryLimit,
entries: make(map[K]*cacheEntry[V]),
}
}

// Put adds the value to the cache, and returns a boolean to indicate whether a value already existed in the cache for that key.
// If there was previously a value, it replaces that value with this one.
// Any Put counts as a refresh in terms of LRU tracking.
func (c *Cache[K, V]) Put(key K, value V) bool {
c.mu.Lock()
defer c.mu.Unlock()

if len(c.entries) == c.entryLimit {
c.evict_locked()
}

entry, alreadyPresent := c.entries[key]
if !alreadyPresent {
entry = &cacheEntry[V]{
value: value,
lastAccess: time.Now(),
}
} else {
entry.value = value
entry.lastAccess = time.Now()
}

c.entries[key] = entry

return alreadyPresent
}

func (c *Cache[K, V]) evict_locked() {
if len(c.entries) == 0 {
return
}
isFirst := true
var oldestKey K
var oldestTimestamp time.Time
for k, v := range c.entries {
if isFirst || v.lastAccess.Before(oldestTimestamp) {
oldestKey = k
oldestTimestamp = v.lastAccess
isFirst = false
}
}
toEvict := c.entries[oldestKey]
c.evictedSuccessfulReads += toEvict.reads
if toEvict.reads == 0 {
c.evictedNeverRead++
}
c.evicted++
delete(c.entries, oldestKey)
}

// Get returns the value assocated with the passed key, and a boolean to indicate whether a value was known or not. If not, nil is returned as the value.
// Any Get counts as a refresh in terms of LRU tracking.
func (c *Cache[K, V]) Get(key K) (*V, bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, present := c.entries[key]
if present {
entry.lastAccess = time.Now()
entry.reads++
return &entry.value, present
} else {
c.unsuccessfulReads++
return nil, false
}
}

func (c *Cache[K, V]) Stats() CacheStats {
c.mu.Lock()
defer c.mu.Unlock()

writtenNeverRead := c.evictedNeverRead

var readsCurrentValues uint64

for _, entry := range c.entries {
readsCurrentValues += entry.reads
if entry.reads == 0 {
writtenNeverRead++
}
}

currentSize := uint64(len(c.entries))

return CacheStats{
sucessfulReadsAllTime: c.evictedSuccessfulReads + readsCurrentValues,
unsuccessfulReadsAllTime: c.unsuccessfulReads,
writtenNeverReadAllTime: writtenNeverRead,
writesAllTime: c.evicted + currentSize,
readsCurrentValues: readsCurrentValues,
currentSize: currentSize,
}
}

type CacheStats struct {
sucessfulReadsAllTime uint64
unsuccessfulReadsAllTime uint64
writtenNeverReadAllTime uint64
writesAllTime uint64
readsCurrentValues uint64
currentSize uint64
}

func (c *CacheStats) HitRate() float64 {
return float64(c.sucessfulReadsAllTime) / (float64(c.sucessfulReadsAllTime) + float64(c.unsuccessfulReadsAllTime))
}

func (c *CacheStats) WrittenNeverRead() uint64 {
return c.writtenNeverReadAllTime
}

func (c *CacheStats) AverageReadCountForCurrentEntries() float64 {
return float64(c.readsCurrentValues) / float64(c.currentSize)
}

func (c *CacheStats) TotalReads() uint64 {
return c.sucessfulReadsAllTime + c.unsuccessfulReadsAllTime
}

func (c *CacheStats) TotalSuccessfulReads() uint64 {
return c.sucessfulReadsAllTime
}

func (c *CacheStats) TotalWrites() uint64 {
return c.writesAllTime
}
157 changes: 157 additions & 0 deletions projects/concurrency/lru_cache_everything_in_buckets/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package cache

import (
"fmt"
"sync"
"sync/atomic"
"testing"

"github.com/stretchr/testify/require"
)

func TestPutThenGet(t *testing.T) {
cache := NewCache[string, string](10)
previouslyExisted := cache.Put("greeting", "hello")
require.False(t, previouslyExisted)

value, present := cache.Get("greeting")
require.True(t, present)
require.Equal(t, "hello", *value)
}

func TestGetMissing(t *testing.T) {
cache := NewCache[string, string](10)
value, present := cache.Get("greeting")
require.False(t, present)
require.Nil(t, value)
}

func TestPutThenOverwriteSameValue(t *testing.T) {
cache := NewCache[string, string](10)
previouslyExisted1 := cache.Put("greeting", "hello")
require.False(t, previouslyExisted1)

previouslyExisted2 := cache.Put("greeting", "hello")
require.True(t, previouslyExisted2)

value, present := cache.Get("greeting")
require.True(t, present)
require.Equal(t, "hello", *value)
}

func TestPutThenOverwriteDifferentValue(t *testing.T) {
cache := NewCache[string, string](10)
previouslyExisted1 := cache.Put("greeting", "hello")
require.False(t, previouslyExisted1)

previouslyExisted2 := cache.Put("greeting", "howdy")
require.True(t, previouslyExisted2)

value, present := cache.Get("greeting")
require.True(t, present)
require.Equal(t, "howdy", *value)
}

func TestEviction_JustWrites(t *testing.T) {
cache := NewCache[string, string](10)

for i := 0; i <= 10; i++ {
cache.Put(fmt.Sprintf("entry-%d", i), "hello")
}
_, present0 := cache.Get("entry-0")
require.False(t, present0)

_, present10 := cache.Get("entry-10")
require.True(t, present10)
}

func TestEviction_ReadsAndWrites(t *testing.T) {
cache := NewCache[string, string](10)

for i := 0; i < 10; i++ {
cache.Put(fmt.Sprintf("entry-%d", i), "hello")
}
_, present0 := cache.Get("entry-0")
require.True(t, present0)

cache.Put("entry-10", "hello")

_, present0 = cache.Get("entry-0")
require.True(t, present0)

_, present1 := cache.Get("entry-1")
require.False(t, present1)

_, present10 := cache.Get("entry-10")
require.True(t, present10)
}

func TestConcurrentPuts(t *testing.T) {
cache := NewCache[string, string](10)

var startWaitGroup sync.WaitGroup
var endWaitGroup sync.WaitGroup

for i := 0; i < 1000; i++ {
startWaitGroup.Add(1)
endWaitGroup.Add(1)
go func(i int) {
startWaitGroup.Wait()
cache.Put(fmt.Sprintf("entry-%d", i), "hello")
endWaitGroup.Done()
}(i)
}
startWaitGroup.Add(-1000)
endWaitGroup.Wait()

sawEntries := 0
for i := 0; i < 1000; i++ {
_, saw := cache.Get(fmt.Sprintf("entry-%d", i))
if saw {
sawEntries++
}
}
require.Equal(t, 10, sawEntries)
}

func TestStats(t *testing.T) {
cache := NewCache[string, string](10)

var startWaitGroup sync.WaitGroup
var endWaitGroup sync.WaitGroup

for i := 0; i < 1000; i++ {
startWaitGroup.Add(1)
endWaitGroup.Add(1)
go func(i int) {
startWaitGroup.Wait()
cache.Put(fmt.Sprintf("entry-%d", i), "hello")
endWaitGroup.Done()
}(i)
}
startWaitGroup.Add(-1000)
endWaitGroup.Wait()

var readWaitGroup sync.WaitGroup
var sawEntries atomic.Uint64
for i := 0; i < 1000; i++ {
readWaitGroup.Add(1)
go func(i int) {
_, saw := cache.Get(fmt.Sprintf("entry-%d", i))
if saw {
sawEntries.Add(1)
}
readWaitGroup.Done()
}(i)
}
readWaitGroup.Wait()
require.Equal(t, uint64(10), sawEntries.Load())

stats := cache.Stats()
require.Equal(t, 0.01, stats.HitRate())
require.Equal(t, uint64(990), stats.WrittenNeverRead())
require.Equal(t, float64(1), stats.AverageReadCountForCurrentEntries())
require.Equal(t, uint64(1000), stats.TotalReads())
require.Equal(t, uint64(10), stats.TotalSuccessfulReads())
require.Equal(t, uint64(1000), stats.TotalWrites())
}
11 changes: 11 additions & 0 deletions projects/concurrency/lru_cache_everything_in_buckets/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/CodeYourFuture/immersive-go-course/projects/concurrency/lru_cache_everything_in_buckets

go 1.21.5

require github.com/stretchr/testify v1.8.4

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions projects/concurrency/lru_cache_everything_in_buckets/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 99be1dd

Please sign in to comment.