init
This commit is contained in:
commit
febcffda68
4 changed files with 196 additions and 0 deletions
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
This is queue data structure designed
|
||||||
|
by Bryan C. Mills (https://github.com/bcmills)
|
||||||
|
and showcased in talk "Rethinking Classical Concurrency Patterns"
|
||||||
|
(https://www.youtube.com/watch?v=5zXAHh5tJqQ)
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/fotonmoton/queue
|
||||||
|
|
||||||
|
go 1.16
|
85
queue.go
Normal file
85
queue.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
// This is queue data structure designed
|
||||||
|
// by Bryan C. Mills (https://github.com/bcmills)
|
||||||
|
// and showcased in talk "Rethinking Classical Concurrency Patterns"
|
||||||
|
// (https://www.youtube.com/watch?v=5zXAHh5tJqQ)
|
||||||
|
package queue
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
val interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This type represents request for content from queue.
|
||||||
|
// When n number of items become avaliable in a queue
|
||||||
|
// they will be send to ch channel for caller to consume.
|
||||||
|
type waiter struct {
|
||||||
|
n int
|
||||||
|
ch chan []Item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holds all requests for items and all items
|
||||||
|
type state struct {
|
||||||
|
waiters []waiter
|
||||||
|
items []Item
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the interesting part. To make this queue goroutine-safe state of the
|
||||||
|
// queue should be owned by one goroutine at a time. One option is to use locks
|
||||||
|
// but here channel used as synchronization mechanism. Queue holds state chanel
|
||||||
|
// with capacity of 1. Because first read from state channel will be nonblocking
|
||||||
|
// and all subsequent calls will wait until state value become avaliable this
|
||||||
|
// prevents data races and makes this type safe to use concurrently.
|
||||||
|
// What an ingenious design!
|
||||||
|
type Queue struct {
|
||||||
|
state chan state
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQueue() *Queue {
|
||||||
|
queue := Queue{state: make(chan state, 1)}
|
||||||
|
// Shared state that will be passed
|
||||||
|
// betwee all goroutines to synchronize access
|
||||||
|
queue.state <- state{}
|
||||||
|
return &queue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) Put(item Item) {
|
||||||
|
// This read from state channel works like mutex.Lock() call.
|
||||||
|
// No one can modify sate until we put it back to channel
|
||||||
|
state := <-q.state
|
||||||
|
state.items = append(state.items, item)
|
||||||
|
// If state has waiting requests and queue has enough items
|
||||||
|
// for first request we send it to waiter channel
|
||||||
|
for len(state.waiters) > 0 {
|
||||||
|
waiter := state.waiters[0]
|
||||||
|
if waiter.n < len(state.items) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// This channel is blocking Get* calls until we put
|
||||||
|
// requested number of items to it
|
||||||
|
waiter.ch <- state.items[:waiter.n:waiter.n]
|
||||||
|
state.items = state.items[waiter.n:]
|
||||||
|
state.waiters = state.waiters[1:]
|
||||||
|
}
|
||||||
|
// Release state for another goroutines to use
|
||||||
|
q.state <- state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) GetMany(n int) []Item {
|
||||||
|
// Acquire exclusive right to modify state
|
||||||
|
state := <-q.state
|
||||||
|
// We can return items right away without creating a waiter
|
||||||
|
if len(state.waiters) == 0 && len(state.items) >= n {
|
||||||
|
items := state.items[:n:n]
|
||||||
|
state.items = state.items[n:]
|
||||||
|
q.state <- state
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
ch := make(chan []Item)
|
||||||
|
state.waiters = append(state.waiters, waiter{n, ch})
|
||||||
|
q.state <- state
|
||||||
|
// Wait for Put call to push items to ch channel
|
||||||
|
return <-ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) Get() Item {
|
||||||
|
return q.GetMany(1)[0]
|
||||||
|
}
|
104
queue_test.go
Normal file
104
queue_test.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package queue
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPut(t *testing.T) {
|
||||||
|
q := NewQueue()
|
||||||
|
q.Put(Item{1})
|
||||||
|
if q.Get().val.(int) != 1 {
|
||||||
|
t.Fatal("wrong item in queue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutMultiple(t *testing.T) {
|
||||||
|
q := NewQueue()
|
||||||
|
q.Put(Item{1})
|
||||||
|
q.Put(Item{2})
|
||||||
|
first, second := q.Get().val.(int), q.Get().val.(int)
|
||||||
|
if first != 1 || second != 2 {
|
||||||
|
t.Fatal("wrong item in queue or wrong order")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyGet(t *testing.T) {
|
||||||
|
|
||||||
|
result := func() chan Item {
|
||||||
|
q := NewQueue()
|
||||||
|
c := make(chan Item)
|
||||||
|
go func() { c <- q.Get() }()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-result():
|
||||||
|
log.Fatal("empty queue should block")
|
||||||
|
case <-time.After(time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMany(t *testing.T) {
|
||||||
|
q := NewQueue()
|
||||||
|
|
||||||
|
result2 := func() chan []Item {
|
||||||
|
c := make(chan []Item)
|
||||||
|
go func() { c <- q.GetMany(2) }()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
q.Put(Item{1})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-result2():
|
||||||
|
log.Fatal("GetMany should block if not enough items in queue")
|
||||||
|
case <-time.After(time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
// this call unblocks first GetMany call and empties queue
|
||||||
|
q.Put(Item{2})
|
||||||
|
// Put enough items in queue for result2 not to block
|
||||||
|
q.Put(Item{3})
|
||||||
|
q.Put(Item{4})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case res := <-result2():
|
||||||
|
third, fourth := res[0].val.(int), res[1].val.(int)
|
||||||
|
if third != 3 || fourth != 4 {
|
||||||
|
t.Fatal("wrong item in queue or wrong order")
|
||||||
|
}
|
||||||
|
case <-time.After(time.Millisecond):
|
||||||
|
log.Fatal("GetMany shouldn't block when queue has enough items")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrent(t *testing.T) {
|
||||||
|
q := NewQueue()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
sum := 0
|
||||||
|
|
||||||
|
wg.Add(2000)
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
q.Put(Item{i})
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
sum += q.Get().val.(int)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if sum != 1000*(0+999)/2 {
|
||||||
|
log.Fatalf("data race. Sum: %v", sum)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue