Skip to content

Instantly share code, notes, and snippets.

@shovon
Last active September 4, 2023 20:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shovon/1484e82062738adbdf9b0cce63e77692 to your computer and use it in GitHub Desktop.
Save shovon/1484e82062738adbdf9b0cce63e77692 to your computer and use it in GitHub Desktop.

Bounded Variable

Think of a bounded variable no different than a bounded queue to solve the consumer—producer problem.

A very common use case is for me is to push to a single variable from one producer, and pull from that single variable from one consumer.

Go has a very simple solution: channels.

But I was curious: can we improve the performance?

Turns out, you can't.

Channels are the winner.

goos: darwin
goarch: arm64
pkg: boundedbuffer
BenchmarkBoundedVariable-8   	    4586	    251930 ns/op	     249 B/op	       5 allocs/op
BenchmarkChannel-8           	    9801	    121622 ns/op	     144 B/op	       2 allocs/op
PASS
	boundedbuffer	coverage: 92.3% of statements
ok  	boundedbuffer	3.252s
package main
import (
"errors"
"sync"
)
var ErrClosed = errors.New("closed")
type BoundedVariable[V any] struct {
lock sync.Locker
hasValueCondition *sync.Cond
noValueCondition *sync.Cond
hasValue bool
value V
isClosed bool
}
func NewBoundedVariable[V any]() *BoundedVariable[V] {
lock := &sync.Mutex{}
return &BoundedVariable[V]{
lock: lock,
hasValueCondition: sync.NewCond(lock),
noValueCondition: sync.NewCond(lock),
}
}
func (bv *BoundedVariable[V]) Get() (V, bool) {
bv.lock.Lock()
defer bv.lock.Unlock()
for !bv.hasValue && !bv.isClosed {
bv.hasValueCondition.Wait()
if bv.isClosed {
var zero V
return zero, false
}
}
bv.hasValue = false
defer bv.noValueCondition.Signal()
return bv.value, true
}
func (bv *BoundedVariable[V]) Set(v V) bool {
bv.lock.Lock()
defer bv.lock.Unlock()
for bv.hasValue && !bv.isClosed {
bv.noValueCondition.Wait()
if bv.isClosed {
return false
}
}
bv.hasValue = true
bv.value = v
defer bv.hasValueCondition.Signal()
return true
}
func (bv *BoundedVariable[V]) Close() {
bv.isClosed = true
bv.hasValueCondition.Broadcast()
bv.noValueCondition.Broadcast()
}
func (bv BoundedVariable[V]) IsClosed() bool {
return bv.isClosed
}
package main
import "testing"
const count = 1000
var intslice []int
func init() {
intslice = make([]int, count)
for i := 0; i < count; i++ {
intslice[i] = i
}
}
func boundedVariable[V any](s []V) *BoundedVariable[V] {
bv := NewBoundedVariable[V]()
go func() {
defer bv.Close()
for _, v := range s {
bv.Set(v)
}
}()
return bv
}
func channel[V any](s []V) chan V {
c := make(chan V)
go func() {
defer close(c)
for _, v := range s {
c <- v
}
}()
return c
}
func BenchmarkBoundedVariable(b *testing.B) {
for i := 0; i < b.N; i++ {
bv := boundedVariable[int](intslice)
for {
if v, ok := bv.Get(); ok {
_ = v
} else {
break
}
}
}
}
func BenchmarkChannel(b *testing.B) {
for i := 0; i < b.N; i++ {
c := channel[int](intslice)
for {
v, ok := <-c
if !ok {
break
}
_ = v
}
}
}
module boundedbuffer
go 1.20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment