Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions slice_sharded.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Package threadsafe implements thread-safe operations.
package threadsafe

import (
"sync/atomic"
)

// ShardedSlice is a high-throughput thread-safe buffer that splits its storage into several
// independent shards. Each shard is a full Slice implementation, so operations on different
// shards proceed in parallel with zero contention.
//
// Using a ShardedSlice will reduce lock contention, retain atomicity at the API level, and
// allow for simple fall-back to a single mutex-backed slice.
//
// The slice returned by Flush() / Peek() concatenates shards in ascending index order. The
// order of items within each shard is preserved, but the overall order is only guaranteed
// per-shard, which is usually acceptable for buffer/queue-like workloads where ordering
// across goroutines is not critical.
//
// All methods are wait-free with bounded work and require no global locks.
type ShardedSlice[T any] struct {
shards []Slice[T]
counter uint64 // used for round-robin shard selection in Append
}

// Append adds the items to one of the shards, selected in a round-robin
// manner using an atomic counter. This ensures good key distribution without
// requiring hashing the items themselves.
func (s *ShardedSlice[T]) Append(item ...T) {
idx := int(atomic.AddUint64(&s.counter, 1)-1) % len(s.shards)
s.shards[idx].Append(item...)
}

// Flush atomically retrieves and clears all shards, concatenating the results into a single slice.
func (s *ShardedSlice[T]) Flush() []T {
// First pass: determine total length
total := 0
for _, sh := range s.shards {
total += sh.Len()
}

out := make([]T, 0, total)
for _, sh := range s.shards {
out = append(out, sh.Flush()...)
}
return out
}

// Peek returns a copy of the current contents of all shards without clearing them.
func (s *ShardedSlice[T]) Peek() []T {
total := 0
for _, sh := range s.shards {
total += sh.Len()
}
out := make([]T, 0, total)
for _, sh := range s.shards {
out = append(out, sh.Peek()...)
}
return out
}

// Len returns the combined length of all shards.
func (s *ShardedSlice[T]) Len() int {
total := 0
for _, sh := range s.shards {
total += sh.Len()
}
return total
}

// NewShardedSlice creates a ShardedSlice with the given number of shards.
// Each shard is pre-allocated with initialCap capacity. shardCount must be
// >0; if <=0, it is coerced to 1.
func NewShardedSlice[T any](shardCount, initialCap int) *ShardedSlice[T] {
if shardCount <= 0 {
shardCount = 1
}
shards := make([]Slice[T], shardCount)
for i := 0; i < shardCount; i++ {
// Use a minimal internal implementation – simple mutex slice.
shards[i] = NewRWMutexSlice[T](initialCap)
}
return &ShardedSlice[T]{shards: shards}
}
44 changes: 44 additions & 0 deletions slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ func TestSliceImplementations(t *testing.T) {
}
runSliceTestSuite(t, suite)
})
t.Run("ShardedSlice", func(t *testing.T) {
suite := &sliceTestSuite[string]{
newSlice: func() Slice[string] {
return NewShardedSlice[string](0, 16)
},
item1: "apple", item2: "banana", item3: "cherry",
items: []string{"apple", "banana", "cherry", "orange", "lime"},
}
runSliceTestSuite(t, suite)
})
})

t.Run("int", func(t *testing.T) {
Expand All @@ -130,6 +140,16 @@ func TestSliceImplementations(t *testing.T) {
}
runSliceTestSuite(t, suite)
})
t.Run("ShardedSlice", func(t *testing.T) {
suite := &sliceTestSuite[int]{
newSlice: func() Slice[int] {
return NewShardedSlice[int](0, 16)
},
item1: 1, item2: 2, item3: 3,
items: []int{1, 2, 3, 4, 5},
}
runSliceTestSuite(t, suite)
})
})

type testStruct struct {
Expand Down Expand Up @@ -173,6 +193,24 @@ func TestSliceImplementations(t *testing.T) {
}
runSliceTestSuite(t, suite)
})
t.Run("ShardedSlice", func(t *testing.T) {
suite := &sliceTestSuite[testStruct]{
newSlice: func() Slice[testStruct] {
return NewShardedSlice[testStruct](0, 16)
},
item1: testStruct{1, "A"},
item2: testStruct{2, "B"},
item3: testStruct{3, "C"},
items: []testStruct{
{1, "A"},
{2, "B"},
{3, "C"},
{4, "D"},
{5, "E"},
},
}
runSliceTestSuite(t, suite)
})
})
}

Expand Down Expand Up @@ -238,4 +276,10 @@ func BenchmarkSliceImplementations(b *testing.B) {
return NewRWMutexSlice[string](0)
})
})

b.Run("ShardedSlice", func(b *testing.B) {
benchmarkSlice(b, func() Slice[string] {
return NewShardedSlice[string](0, 16)
})
})
}