adding set interface and threadsafe implementation

This commit is contained in:
2026-07-24 23:29:07 -04:00
parent 4ea82a9ed2
commit 5ae74c0ea0
9 changed files with 104858 additions and 258 deletions
+81
View File
@@ -0,0 +1,81 @@
package threadsafeset
import (
"sync"
"code.wmdillon.com/wmdillon/set/simpleset"
)
type Set[T comparable] struct {
mutex sync.RWMutex
set *simpleset.Set[T]
}
func New[T comparable](elements ...T) *Set[T] {
s := &Set[T]{
set: simpleset.New(elements...),
}
return s
}
func (s *Set[T]) Add(element T) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.set.Add(element)
}
func (s *Set[T]) Remove(element T) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.set.Remove(element)
}
func (s *Set[T]) Contains(element T) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.set.Contains(element)
}
func (s *Set[T]) Size() int {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.set.Size()
}
func (s *Set[T]) ToSlice() []T {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.set.ToSlice()
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Equal(other *Set[T]) bool {
this.mutex.RLock()
defer this.mutex.RUnlock()
return this.set.Equal(other.set)
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Union(other *Set[T]) *Set[T] {
this.mutex.RLock()
defer this.mutex.RUnlock()
return &Set[T]{set: this.set.Union(other.set)}
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Intersection(other *Set[T]) *Set[T] {
this.mutex.RLock()
defer this.mutex.RUnlock()
return &Set[T]{set: this.set.Intersection(other.set)}
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Difference(other *Set[T]) *Set[T] {
this.mutex.RLock()
defer this.mutex.RUnlock()
return &Set[T]{set: this.set.Difference(other.set)}
}
+137
View File
@@ -0,0 +1,137 @@
package threadsafeset
import (
"bufio"
"context"
"math/rand/v2"
"os"
"runtime"
"slices"
"strings"
"sync"
"testing"
"time"
)
var Words = func() []string {
f, err := os.Open("../words")
if err != nil {
panic("error opening ../words: " + err.Error())
}
defer f.Close()
scanner := bufio.NewScanner(f)
results := make([]string, 0)
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); len(line) > 0 {
results = append(results, line)
}
}
if err := scanner.Err(); err != nil {
panic("error from scanner.Err(): " + err.Error())
}
return results
}()
func CloneWords() []string {
results := make([]string, 0, len(Words))
for _, word := range Words {
results = append(results, strings.Clone(word))
}
return results
}
func RandomWord() string {
return Words[rand.IntN(len(Words))]
}
func ContextNotDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return false
default:
return true
}
}
func TestAdd(t *testing.T) {
set := New[string]()
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(t.Context(), time.Second*2)
defer cancel()
for range runtime.NumCPU() * 10 {
wg.Go(func() {
for i := 0; i < len(Words) && ContextNotDone(ctx); i++ {
set.Add(Words[i])
}
})
}
wg.Wait()
}
func TestContains(t *testing.T) {
set := New(Words...)
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(t.Context(), time.Second*2)
defer cancel()
for range runtime.NumCPU() * 10 {
wg.Go(func() {
for i := 0; i < len(Words) && ContextNotDone(ctx); i++ {
if !set.Contains(Words[i]) {
panic("error: " + Words[i] + " not found")
}
}
})
}
wg.Wait()
}
func TestRemove(t *testing.T) {
set := New(Words...)
var wg sync.WaitGroup
for range runtime.NumCPU() * 10 {
wg.Go(func() {
for _, word := range Words {
set.Remove(word)
}
})
}
wg.Wait()
}
func TestSize(t *testing.T) {
set := New[string]()
for index, word := range Words {
if want, got := index, set.Size(); want != got {
t.Fatalf("error: wanted size %d; got %d\n", want, got)
}
set.Add(word)
if want, got := index+1, set.Size(); want != got {
t.Fatalf("error: wanted size %d; got %d\n", want, got)
}
}
}
func TestToSlice(t *testing.T) {
want := CloneWords()
slices.Sort(want)
set := New(Words...)
got := set.ToSlice()
slices.Sort(got)
if !slices.Equal(want, got) {
t.Fatalf("error: mismatched ToSlice() output\n")
}
}
func BenchmarkAdd(b *testing.B) {
set := New[string]()
for b.Loop() {
set.Add(RandomWord())
}
}
func BenchmarkContains(b *testing.B) {
set := New(Words...)
for b.Loop() {
set.Contains(RandomWord())
}
}