├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── error.go ├── go.mod ├── go.sum ├── mpmc.go ├── mpmc_test.go ├── mpsc.go ├── mpsc_test.go ├── ringbuffer.go ├── spmc.go ├── spmc_test.go ├── spsc.go └── spsc_test.go /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 tink . All rights reserved. 2 | # Use of this source code is governed by a MIT style 3 | # license that can be found in the LICENSE file. 4 | 5 | run: 6 | timeout: 10m 7 | linters: 8 | disable-all: true 9 | enable: 10 | - misspell 11 | - ineffassign 12 | - typecheck 13 | - varcheck 14 | - unused 15 | - structcheck 16 | - deadcode 17 | - gosimple 18 | - goimports 19 | - errcheck 20 | - staticcheck 21 | - stylecheck 22 | - gosec 23 | - asciicheck 24 | - bodyclose 25 | - exportloopref 26 | - rowserrcheck 27 | - unconvert 28 | - makezero 29 | - durationcheck 30 | - prealloc 31 | - predeclared 32 | - govet 33 | - dogsled 34 | - sqlclosecheck 35 | - whitespace 36 | 37 | linters-settings: 38 | staticcheck: 39 | checks: ["S1002","S1004","S1007","S1009","S1010","S1012","S1019","S1020","S1021","S1024","S1030","SA2*","SA3*","SA4009","SA5*","SA6000","SA6001","SA6005", "-SA2002"] 40 | stylecheck: 41 | checks: ["-ST1003"] 42 | gosec: 43 | severity: "low" 44 | confidence: "low" 45 | excludes: 46 | - G101 47 | issues: 48 | exclude-rules: 49 | - path: _test\.go 50 | linters: 51 | - errcheck 52 | - gosec 53 | - rowserrcheck 54 | - makezero 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 cyub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tidy fmt lint tools 2 | 3 | GO = go 4 | GOFILES = $(shell find . -name "*.go") 5 | 6 | tidy: 7 | $(GO) mod tidy 8 | 9 | fmt: 10 | gofmt -s -l -w $(GOFILES) 11 | 12 | lint: 13 | golangci-lint run . 14 | 15 | tools: 16 | $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ringbuffer 2 | 3 | ![GitHub](https://img.shields.io/github/license/cyub/ringbuffer) 4 | 5 | Lock-free ring buffer in Go, support SPSC/SPMC/MPSC/MPMC implementations. 6 | 7 | - SPSC - Single Producer/Single Consumer 8 | - SPMC - Single Producer/Multi-Consumer 9 | - MPSC - Multi-Producer/Single Consumer 10 | - MPMC - Multi-Producer/Multi-Consumer 11 | 12 | ## Features 13 | 14 | - Lock-free operations - they succeed or fail immediately without blocking or waiting. 15 | - Thread-safe direct access to the internal ring buffer memory. 16 | - Support SPSC/SPMC/MPSC/MPMC implementations. You can choose the best performing implementation based on your business scenario 17 | 18 | 19 | ## Benchmark 20 | 21 | Machine information for benchmarks: 22 | 23 | Apple M1 Pro 8 core 24 | 25 | ### MPMC ringbuffer vs channel 26 | 27 | ```bash 28 | go test -benchmem -run=^$ -bench="^BenchmarkRingMPMC|BenchmarkChanMPMC$" . github.com/cyub/ringbuffer 29 | ``` 30 | 31 | ``` 32 | goos: darwin 33 | goarch: arm64 34 | pkg: github.com/cyub/ringbuffer 35 | BenchmarkRingMPMC/100P100C-8 12364594 95.92 ns/op 7 B/op 0 allocs/op 36 | BenchmarkRingMPMC/4P4C_1CPU-8 36860979 28.96 ns/op 8 B/op 1 allocs/op 37 | BenchmarkChanMPMC/100P100C-8 9417372 110.0 ns/op 7 B/op 0 allocs/op 38 | BenchmarkChanMPMC/4P4C_1CPU-8 31175139 36.84 ns/op 8 B/op 1 allocs/op 39 | PASS 40 | ok github.com/cyub/ringbuffer 4.899s 41 | ``` 42 | 43 | ### MPSC ringbuffer vs channel 44 | 45 | ```bash 46 | go test -benchmem -run=^$ -bench="^BenchmarkRingMPSC|BenchmarkChanMPSC$" . github.com/cyub/ringbuffer 47 | ``` 48 | 49 | ``` 50 | goos: darwin 51 | goarch: arm64 52 | pkg: github.com/cyub/ringbuffer 53 | BenchmarkRingMPSC/100P1C-8 13354032 88.34 ns/op 8 B/op 1 allocs/op 54 | BenchmarkRingMPSC/4P1C_1CPU-8 38775084 29.59 ns/op 8 B/op 1 allocs/op 55 | BenchmarkChanMPSC/100P1C-8 12371011 104.6 ns/op 8 B/op 1 allocs/op 56 | BenchmarkChanMPSC/4P1C_1CPU-8 28364574 38.06 ns/op 8 B/op 1 allocs/op 57 | PASS 58 | ok github.com/cyub/ringbuffer 5.113s 59 | ``` 60 | 61 | ### SPMC ringbuffer vs channel 62 | 63 | ```bash 64 | go test -benchmem -run=^$ -bench="^BenchmarkRingSPMC|BenchmarkChanSPMC$" . github.com/cyub/ringbuffer 65 | ``` 66 | 67 | ``` 68 | goos: darwin 69 | goarch: arm64 70 | pkg: github.com/cyub/ringbuffer 71 | BenchmarkRingSPMC/1P100C-8 15812037 76.08 ns/op 7 B/op 0 allocs/op 72 | BenchmarkRingSPMC/1P4C_1CPU-8 52281694 20.65 ns/op 8 B/op 1 allocs/op 73 | BenchmarkChanSPMC/1P100C-8 4912020 437.0 ns/op 7 B/op 0 allocs/op 74 | BenchmarkChanSPMC/1P4C_1CPU-8 29503178 36.53 ns/op 8 B/op 1 allocs/op 75 | PASS 76 | ok github.com/cyub/ringbuffer 6.033s 77 | ``` 78 | 79 | ### SPSC ringbuffer vs channel 80 | 81 | ```bash 82 | go test -benchmem -run=^$ -bench="^BenchmarkRingSPSC|BenchmarkChanSPSC$" . github.com/cyub/ringbuffer 83 | ``` 84 | 85 | ``` 86 | goos: darwin 87 | goarch: arm64 88 | pkg: github.com/cyub/ringbuffer 89 | BenchmarkRingSPSC/1P1C-8 16629625 68.52 ns/op 7 B/op 0 allocs/op 90 | BenchmarkRingSPSC/1P1C_1CPU-8 44770410 24.89 ns/op 8 B/op 1 allocs/op 91 | BenchmarkChanSPSC/1P1C-8 8335009 286.3 ns/op 7 B/op 0 allocs/op 92 | BenchmarkChanSPSC/1P1C_1CPU-8 31815259 36.12 ns/op 8 B/op 1 allocs/op 93 | PASS 94 | ok github.com/cyub/ringbuffer 6.204s 95 | ``` -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import "errors" 8 | 9 | var ( 10 | // ErrIsEmpty indicate ring buffer is empty 11 | ErrIsEmpty = errors.New("ring buffer is empty") 12 | // ErrIsFull indicate ring buffer is full 13 | ErrIsFull = errors.New("ring buffer is full") 14 | ) 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cyub/ringbuffer 2 | 3 | go 1.16 4 | 5 | require github.com/stretchr/testify v1.8.1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 12 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /mpmc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "runtime" 9 | "sync/atomic" 10 | "unsafe" 11 | ) 12 | 13 | // MpmcRingBuffer define Multi-Producer/Multi-Consumer ring buffer 14 | type MpmcRingBuffer struct { 15 | ringbuffer 16 | } 17 | 18 | var _ RingBuffer = (*MpmcRingBuffer)(nil) 19 | 20 | // NewMpmcRingBuffer return the mpmc ring buffer with specified capacity 21 | func NewMpmcRingBuffer(capacity int) *MpmcRingBuffer { 22 | return &MpmcRingBuffer{ 23 | ringbuffer{ 24 | head: 0, 25 | tail: 0, 26 | capacity: capacity, 27 | elements: make([]interface{}, capacity), 28 | }, 29 | } 30 | } 31 | 32 | // Enqueue element to the ring buffer 33 | // if the ring buffer is full, then return ErrIsFull. 34 | // When equeue element to the ring buffer, it may happen that the consumer who are consuming the same slot, so an error is returned 35 | func (q *MpmcRingBuffer) Enqueue(elem interface{}) error { 36 | if elem == nil { 37 | elem = nilPlaceholder 38 | } 39 | 40 | for { 41 | h := atomic.LoadUint64(&q.head) 42 | t := atomic.LoadUint64(&q.tail) 43 | if t >= h+uint64(q.capacity) { 44 | return ErrIsFull 45 | } 46 | 47 | slot := (*eface)(unsafe.Pointer(&q.elements[t%uint64(q.capacity)])) 48 | if atomic.LoadPointer(&slot.typ) != nil { 49 | runtime.Gosched() 50 | continue 51 | } 52 | if !atomic.CompareAndSwapUint64(&q.tail, t, t+1) { 53 | runtime.Gosched() 54 | continue 55 | } 56 | 57 | eval := *(*eface)(unsafe.Pointer(&elem)) 58 | atomic.StorePointer(&slot.typ, eval.typ) 59 | atomic.StorePointer(&slot.val, eval.val) 60 | return nil 61 | } 62 | } 63 | 64 | // Dequeue an element from the ring buffer 65 | // if the ring buffer is empty, then return ErrIsEmpty 66 | // When dequeue element to the ring buffer, it may happen that the producer who are working the same slot, so an error is returned 67 | func (q *MpmcRingBuffer) Dequeue() (interface{}, error) { 68 | for { 69 | h := atomic.LoadUint64(&q.head) 70 | t := atomic.LoadUint64(&q.tail) 71 | if t == h { 72 | return nil, ErrIsEmpty 73 | } 74 | 75 | slot := (*eface)(unsafe.Pointer(&q.elements[h%uint64(q.capacity)])) 76 | if atomic.LoadPointer(&slot.val) == nil { 77 | runtime.Gosched() 78 | continue 79 | } 80 | if !atomic.CompareAndSwapUint64(&q.head, h, h+1) { 81 | runtime.Gosched() 82 | continue 83 | } 84 | elem := *(*interface{})(unsafe.Pointer(slot)) 85 | atomic.StorePointer(&slot.val, nil) 86 | atomic.StorePointer(&slot.typ, nil) 87 | if elem == nilPlaceholder { 88 | return nil, nil 89 | } 90 | return elem, nil 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /mpmc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "os" 9 | "runtime" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestMPMCRingBuffer(t *testing.T) { 17 | cap := 16 18 | spsc := NewMpmcRingBuffer(cap) 19 | 20 | assert.Equal(t, spsc.Length(), 0) 21 | assert.Equal(t, spsc.Capacity(), cap) 22 | 23 | for i := 0; i < cap*2; i++ { 24 | err := spsc.Enqueue(i) 25 | if i < cap { 26 | assert.ErrorIs(t, err, nil) 27 | } else { 28 | assert.ErrorIs(t, err, ErrIsFull) 29 | } 30 | } 31 | 32 | assert.Equal(t, spsc.Length(), cap) 33 | assert.Equal(t, spsc.Capacity(), cap) 34 | for i := 0; i < cap*2; i++ { 35 | _, err := spsc.Dequeue() 36 | if i < cap { 37 | assert.ErrorIs(t, err, nil) 38 | } else { 39 | assert.ErrorIs(t, err, ErrIsEmpty) 40 | } 41 | } 42 | assert.Equal(t, spsc.Length(), 0) 43 | assert.Equal(t, spsc.Capacity(), cap) 44 | } 45 | 46 | func mknumslice(n int) []int { 47 | var s = make([]int, n) 48 | for i := range s { 49 | s[i] = i 50 | } 51 | return s 52 | } 53 | 54 | func doneEnv() bool { 55 | switch os.Getenv("DONE") { 56 | case "", "true": 57 | return true 58 | case "false": 59 | return false 60 | default: 61 | return true 62 | } 63 | } 64 | 65 | func benchmarkRingBuffer(b *testing.B, ring RingBuffer, workers int, multiProducer, multiConsumer bool, done bool) { 66 | var size = b.N/workers + 1 67 | var numbers = mknumslice(size) 68 | var wg sync.WaitGroup 69 | 70 | wg.Add(workers * 2) 71 | b.ResetTimer() 72 | 73 | if multiProducer { 74 | for p := 0; p < workers; p++ { 75 | go func() { 76 | for i := range numbers { 77 | retry: 78 | if err := ring.Enqueue(i); err != nil { 79 | if done { // all must done 80 | runtime.Gosched() 81 | goto retry 82 | } 83 | } 84 | } 85 | wg.Done() 86 | }() 87 | } 88 | } else { 89 | go func() { 90 | for p := 0; p < workers; p++ { 91 | for i := range numbers { 92 | retry: 93 | if err := ring.Enqueue(i); err != nil { 94 | if done { 95 | runtime.Gosched() 96 | goto retry 97 | } 98 | } 99 | } 100 | wg.Done() 101 | } 102 | }() 103 | 104 | } 105 | 106 | if multiConsumer { 107 | for p := 0; p < workers; p++ { 108 | go func() { 109 | for i := 0; i < size; i++ { 110 | retry: 111 | if _, err := ring.Dequeue(); err != nil { 112 | if done { 113 | runtime.Gosched() 114 | goto retry 115 | } 116 | 117 | } 118 | } 119 | wg.Done() 120 | }() 121 | } 122 | } else { 123 | go func() { 124 | for p := 0; p < workers; p++ { 125 | for i := 0; i < size; i++ { 126 | retry: 127 | if _, err := ring.Dequeue(); err != nil { 128 | if done { 129 | runtime.Gosched() 130 | goto retry 131 | } 132 | } 133 | } 134 | wg.Done() 135 | } 136 | }() 137 | } 138 | wg.Wait() 139 | } 140 | 141 | func BenchmarkRingMPMC(b *testing.B) { 142 | b.Run("100P100C", func(b *testing.B) { 143 | benchmarkRingBuffer(b, NewMpmcRingBuffer(8192), 100, true, true, doneEnv()) 144 | }) 145 | b.Run("4P4C_1CPU", func(b *testing.B) { 146 | pp := runtime.GOMAXPROCS(1) 147 | benchmarkRingBuffer(b, NewMpmcRingBuffer(8192), 4, true, true, doneEnv()) 148 | runtime.GOMAXPROCS(pp) 149 | }) 150 | } 151 | 152 | func BenchmarkChanMPMC(b *testing.B) { 153 | b.Run("100P100C", func(b *testing.B) { 154 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 100, true, true, doneEnv()) 155 | }) 156 | b.Run("4P4C_1CPU", func(b *testing.B) { 157 | pp := runtime.GOMAXPROCS(1) 158 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 4, true, true, doneEnv()) 159 | runtime.GOMAXPROCS(pp) 160 | }) 161 | } 162 | 163 | // ringbuffer base channel as compare object 164 | type chanRingBuffer chan interface{} 165 | 166 | func newChanRingBuffer(capacity int) chanRingBuffer { 167 | return make(chanRingBuffer, capacity) 168 | } 169 | func (c chanRingBuffer) Enqueue(elem interface{}) error { 170 | select { 171 | case c <- elem: 172 | return nil 173 | default: 174 | return ErrIsFull 175 | } 176 | } 177 | 178 | func (c chanRingBuffer) Dequeue() (interface{}, error) { 179 | select { 180 | case elem := <-c: 181 | return elem, nil 182 | default: 183 | return nil, ErrIsEmpty 184 | } 185 | } 186 | 187 | func (c chanRingBuffer) Length() int { 188 | return len(c) 189 | } 190 | 191 | func (c chanRingBuffer) Capacity() int { 192 | return cap(c) 193 | } 194 | -------------------------------------------------------------------------------- /mpsc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "runtime" 9 | "sync/atomic" 10 | "unsafe" 11 | ) 12 | 13 | // MpscRingBuffer define Multi-Producer/Single Consumer ring buffer 14 | type MpscRingBuffer struct { 15 | ringbuffer 16 | } 17 | 18 | var _ RingBuffer = (*MpscRingBuffer)(nil) 19 | 20 | // NewMpscRingBuffer return the mpsc ring buffer with specified capacity 21 | func NewMpscRingBuffer(capacity int) *MpscRingBuffer { 22 | return &MpscRingBuffer{ 23 | ringbuffer{ 24 | head: 0, 25 | tail: 0, 26 | capacity: capacity, 27 | elements: make([]interface{}, capacity), 28 | }, 29 | } 30 | } 31 | 32 | // Enqueue element to the ring buffer 33 | // if the ring buffer is full, then return ErrIsFull. 34 | func (q *MpscRingBuffer) Enqueue(elem interface{}) error { 35 | if elem == nil { 36 | elem = nilPlaceholder 37 | } 38 | 39 | for { 40 | h := atomic.LoadUint64(&q.head) 41 | t := atomic.LoadUint64(&q.tail) 42 | // for a queue that is already full, when atomic loaded q.head, other thread processes happen to be dequeued and enqueued sequentially, 43 | // then maybe t is greater than h + q.capacity 44 | if t >= h+uint64(q.capacity) { 45 | return ErrIsFull 46 | } 47 | 48 | slot := (*eface)(unsafe.Pointer(&q.elements[t%uint64(q.capacity)])) 49 | if !atomic.CompareAndSwapUint64(&q.tail, t, t+1) { 50 | runtime.Gosched() 51 | continue 52 | } 53 | 54 | eval := *(*eface)(unsafe.Pointer(&elem)) 55 | atomic.StorePointer(&slot.typ, eval.typ) 56 | atomic.StorePointer(&slot.val, eval.val) 57 | return nil 58 | } 59 | } 60 | 61 | // Dequeue an element from the ring buffer 62 | // if the ring buffer is empty, then return ErrIsEmpty 63 | // When dequeue element to the ring buffer, it may happen that the producer who are working the same slot, so an error is returned 64 | func (q *MpscRingBuffer) Dequeue() (interface{}, error) { 65 | retry: 66 | h := q.head 67 | t := atomic.LoadUint64(&q.tail) 68 | if t == h { 69 | return nil, ErrIsEmpty 70 | } 71 | 72 | idx := h % uint64(q.capacity) 73 | slot := (*eface)(unsafe.Pointer(&q.elements[idx])) 74 | if atomic.LoadPointer(&slot.val) == nil { 75 | goto retry 76 | } 77 | elem := q.elements[idx] 78 | q.elements[idx] = nil 79 | atomic.AddUint64(&q.head, 1) 80 | if elem == nilPlaceholder { 81 | return nil, nil 82 | } 83 | return elem, nil 84 | } 85 | -------------------------------------------------------------------------------- /mpsc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestMPSCRingBuffer(t *testing.T) { 15 | cap := 16 16 | spsc := NewMpscRingBuffer(cap) 17 | 18 | assert.Equal(t, spsc.Length(), 0) 19 | assert.Equal(t, spsc.Capacity(), cap) 20 | 21 | for i := 0; i < cap*2; i++ { 22 | err := spsc.Enqueue(i) 23 | if i < cap { 24 | assert.ErrorIs(t, err, nil) 25 | } else { 26 | assert.ErrorIs(t, err, ErrIsFull) 27 | } 28 | } 29 | 30 | assert.Equal(t, spsc.Length(), cap) 31 | assert.Equal(t, spsc.Capacity(), cap) 32 | for i := 0; i < cap*2; i++ { 33 | _, err := spsc.Dequeue() 34 | if i < cap { 35 | assert.ErrorIs(t, err, nil) 36 | } else { 37 | assert.ErrorIs(t, err, ErrIsEmpty) 38 | } 39 | } 40 | assert.Equal(t, spsc.Length(), 0) 41 | assert.Equal(t, spsc.Capacity(), cap) 42 | } 43 | 44 | func BenchmarkRingMPSC(b *testing.B) { 45 | b.Run("100P1C", func(b *testing.B) { 46 | benchmarkRingBuffer(b, NewMpscRingBuffer(8192), 100, true, false, doneEnv()) 47 | }) 48 | b.Run("4P1C_1CPU", func(b *testing.B) { 49 | pp := runtime.GOMAXPROCS(1) 50 | benchmarkRingBuffer(b, NewMpscRingBuffer(8192), 4, true, false, doneEnv()) 51 | runtime.GOMAXPROCS(pp) 52 | }) 53 | } 54 | 55 | func BenchmarkChanMPSC(b *testing.B) { 56 | b.Run("100P1C", func(b *testing.B) { 57 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 100, true, false, doneEnv()) 58 | }) 59 | b.Run("4P1C_1CPU", func(b *testing.B) { 60 | pp := runtime.GOMAXPROCS(1) 61 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 4, true, false, doneEnv()) 62 | runtime.GOMAXPROCS(pp) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /ringbuffer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "sync/atomic" 9 | "unsafe" 10 | ) 11 | 12 | // RingBuffer is an interface 13 | type RingBuffer interface { 14 | Enqueue(interface{}) error 15 | Dequeue() (interface{}, error) 16 | Length() int 17 | Capacity() int 18 | } 19 | 20 | // eface empty interface 21 | type eface struct { 22 | typ unsafe.Pointer 23 | val unsafe.Pointer 24 | } 25 | 26 | type placeholder struct{} 27 | 28 | var nilPlaceholder interface{} = placeholder{} 29 | 30 | type cacheLinePad struct { 31 | _ [128 - unsafe.Sizeof(uint64(0))%128]byte 32 | } 33 | 34 | // ringbuffer struct 35 | type ringbuffer struct { 36 | head uint64 37 | _ cacheLinePad 38 | tail uint64 39 | _ cacheLinePad 40 | capacity int 41 | elements []interface{} 42 | } 43 | 44 | // Length return the number of all elements 45 | func (q *ringbuffer) Length() int { 46 | return int(atomic.LoadUint64(&q.tail) - atomic.LoadUint64(&q.head)) 47 | } 48 | 49 | // Capacity return the capacity of ring buffer 50 | func (q *ringbuffer) Capacity() int { 51 | return q.capacity 52 | } 53 | -------------------------------------------------------------------------------- /spmc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "runtime" 9 | "sync/atomic" 10 | "unsafe" 11 | ) 12 | 13 | // SpmcRingBuffer define Single Producer/Multi-Consumer ring buffer 14 | type SpmcRingBuffer struct { 15 | ringbuffer 16 | } 17 | 18 | var _ RingBuffer = (*SpmcRingBuffer)(nil) 19 | 20 | // NewSpmcRingBuffer return the spmc ring buffer with specified capacity 21 | func NewSpmcRingBuffer(capacity int) *SpmcRingBuffer { 22 | return &SpmcRingBuffer{ 23 | ringbuffer{ 24 | head: 0, 25 | tail: 0, 26 | capacity: capacity, 27 | elements: make([]interface{}, capacity), 28 | }, 29 | } 30 | } 31 | 32 | // Enqueue element to the ring buffer 33 | // if the ring buffer is full, then return ErrIsFull. 34 | // When equeue element to the ring buffer, it may happen that the consumer who are consuming the same slot, so an error is returned 35 | func (q *SpmcRingBuffer) Enqueue(elem interface{}) error { 36 | if elem == nil { 37 | elem = nilPlaceholder 38 | } 39 | retry: 40 | h := atomic.LoadUint64(&q.head) 41 | t := q.tail 42 | if t >= h+uint64(q.capacity) { 43 | return ErrIsFull 44 | } 45 | 46 | slot := (*eface)(unsafe.Pointer(&q.elements[t%uint64(q.capacity)])) 47 | if atomic.LoadPointer(&slot.typ) != nil { 48 | goto retry 49 | } 50 | 51 | *(*interface{})(unsafe.Pointer(slot)) = elem 52 | atomic.StoreUint64(&q.tail, t+1) 53 | return nil 54 | } 55 | 56 | // Dequeue an element from the ring buffer 57 | // if the ring buffer is empty, then return ErrIsEmpty 58 | func (q *SpmcRingBuffer) Dequeue() (interface{}, error) { 59 | for { 60 | h := atomic.LoadUint64(&q.head) 61 | t := atomic.LoadUint64(&q.tail) 62 | if t == h { 63 | return nil, ErrIsEmpty 64 | } 65 | if !atomic.CompareAndSwapUint64(&q.head, h, h+1) { 66 | runtime.Gosched() 67 | continue 68 | } 69 | slot := (*eface)(unsafe.Pointer(&q.elements[h%uint64(q.capacity)])) 70 | elem := *(*interface{})(unsafe.Pointer(slot)) 71 | slot.val = nil 72 | atomic.StorePointer(&slot.typ, nil) 73 | if elem == nilPlaceholder { 74 | return nil, nil 75 | } 76 | return elem, nil 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /spmc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSPMCRingBuffer(t *testing.T) { 15 | cap := 16 16 | spsc := NewSpmcRingBuffer(cap) 17 | 18 | assert.Equal(t, spsc.Length(), 0) 19 | assert.Equal(t, spsc.Capacity(), cap) 20 | 21 | for i := 0; i < cap*2; i++ { 22 | err := spsc.Enqueue(i) 23 | if i < cap { 24 | assert.ErrorIs(t, err, nil) 25 | } else { 26 | assert.ErrorIs(t, err, ErrIsFull) 27 | } 28 | } 29 | 30 | assert.Equal(t, spsc.Length(), cap) 31 | assert.Equal(t, spsc.Capacity(), cap) 32 | for i := 0; i < cap*2; i++ { 33 | _, err := spsc.Dequeue() 34 | if i < cap { 35 | assert.ErrorIs(t, err, nil) 36 | } else { 37 | assert.ErrorIs(t, err, ErrIsEmpty) 38 | } 39 | } 40 | assert.Equal(t, spsc.Length(), 0) 41 | assert.Equal(t, spsc.Capacity(), cap) 42 | } 43 | 44 | func BenchmarkRingSPMC(b *testing.B) { 45 | b.Run("1P100C", func(b *testing.B) { 46 | benchmarkRingBuffer(b, NewSpmcRingBuffer(8192), 100, false, true, doneEnv()) 47 | }) 48 | b.Run("1P4C_1CPU", func(b *testing.B) { 49 | pp := runtime.GOMAXPROCS(1) 50 | benchmarkRingBuffer(b, NewSpmcRingBuffer(8192), 4, false, true, doneEnv()) 51 | runtime.GOMAXPROCS(pp) 52 | }) 53 | } 54 | 55 | func BenchmarkChanSPMC(b *testing.B) { 56 | b.Run("1P100C", func(b *testing.B) { 57 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 100, false, true, doneEnv()) 58 | }) 59 | b.Run("1P4C_1CPU", func(b *testing.B) { 60 | pp := runtime.GOMAXPROCS(1) 61 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 4, false, true, doneEnv()) 62 | runtime.GOMAXPROCS(pp) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /spsc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "sync/atomic" 9 | ) 10 | 11 | // SpscRingBuffer define Single Producer/Single Consumer ring buffer 12 | type SpscRingBuffer struct { 13 | ringbuffer 14 | } 15 | 16 | var _ RingBuffer = (*SpscRingBuffer)(nil) 17 | 18 | // NewSpscRingBuffer return the spsc ring buffer with specified capacity 19 | func NewSpscRingBuffer(capacity int) *SpscRingBuffer { 20 | return &SpscRingBuffer{ 21 | ringbuffer{ 22 | head: 0, 23 | tail: 0, 24 | capacity: capacity, 25 | elements: make([]interface{}, capacity), 26 | }, 27 | } 28 | } 29 | 30 | // Enqueue element to the ring buffer 31 | // if the ring buffer is full, then return ErrIsFull 32 | func (q *SpscRingBuffer) Enqueue(elem interface{}) error { 33 | if elem == nil { 34 | elem = nilPlaceholder 35 | } 36 | h := atomic.LoadUint64(&q.head) 37 | t := q.tail 38 | if t >= h+uint64(q.capacity) { 39 | return ErrIsFull 40 | } 41 | 42 | q.elements[t%uint64(q.capacity)] = elem 43 | atomic.AddUint64(&q.tail, 1) 44 | return nil 45 | } 46 | 47 | // Dequeue an element from the ring buffer 48 | // if the ring buffer is empty, then return ErrIsEmpty 49 | func (q *SpscRingBuffer) Dequeue() (interface{}, error) { 50 | h := q.head 51 | t := atomic.LoadUint64(&q.tail) 52 | if t == h { 53 | return nil, ErrIsEmpty 54 | } 55 | 56 | elem := q.elements[h%uint64(q.capacity)] 57 | atomic.AddUint64(&q.head, 1) 58 | if elem == nilPlaceholder { 59 | return nil, nil 60 | } 61 | return elem, nil 62 | } 63 | -------------------------------------------------------------------------------- /spsc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 tink . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package ringbuffer 6 | 7 | import ( 8 | "runtime" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestSPSCRingBuffer(t *testing.T) { 15 | cap := 16 16 | spsc := NewSpscRingBuffer(cap) 17 | 18 | assert.Equal(t, spsc.Length(), 0) 19 | assert.Equal(t, spsc.Capacity(), cap) 20 | 21 | for i := 0; i < cap*2; i++ { 22 | err := spsc.Enqueue(i) 23 | if i < cap { 24 | assert.ErrorIs(t, err, nil) 25 | } else { 26 | assert.ErrorIs(t, err, ErrIsFull) 27 | } 28 | } 29 | 30 | assert.Equal(t, spsc.Length(), cap) 31 | assert.Equal(t, spsc.Capacity(), cap) 32 | 33 | for i := 0; i < cap*2; i++ { 34 | _, err := spsc.Dequeue() 35 | if i < cap { 36 | assert.ErrorIs(t, err, nil) 37 | } else { 38 | assert.ErrorIs(t, err, ErrIsEmpty) 39 | } 40 | } 41 | assert.Equal(t, spsc.Length(), 0) 42 | assert.Equal(t, spsc.Capacity(), cap) 43 | } 44 | 45 | func BenchmarkRingSPSC(b *testing.B) { 46 | b.Run("1P1C", func(b *testing.B) { 47 | benchmarkRingBuffer(b, NewSpscRingBuffer(8192), 100, false, false, doneEnv()) 48 | }) 49 | b.Run("1P1C_1CPU", func(b *testing.B) { 50 | pp := runtime.GOMAXPROCS(1) 51 | benchmarkRingBuffer(b, NewSpscRingBuffer(8192), 4, false, false, doneEnv()) 52 | runtime.GOMAXPROCS(pp) 53 | }) 54 | } 55 | 56 | func BenchmarkChanSPSC(b *testing.B) { 57 | b.Run("1P1C", func(b *testing.B) { 58 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 100, false, false, doneEnv()) 59 | }) 60 | b.Run("1P1C_1CPU", func(b *testing.B) { 61 | pp := runtime.GOMAXPROCS(1) 62 | benchmarkRingBuffer(b, newChanRingBuffer(8192), 4, false, false, doneEnv()) 63 | runtime.GOMAXPROCS(pp) 64 | }) 65 | } 66 | --------------------------------------------------------------------------------