Workiva/go-datastructures

RingBuffer: 100% cpu usage

sheldonlyr opened this issue · 7 comments

$uname -a
Linux Lee-Ubuntu 4.4.0-28-generic #47-Ubuntu SMP Fri Jun 24 10:09:13 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
$cat /etc/issue
Linux Lee-Ubuntu 4.4.0-28-generic #47-Ubuntu SMP Fri Jun 24 10:09:13 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
$go env
Linux Lee-Ubuntu 4.4.0-28-generic #47-Ubuntu SMP Fri Jun 24 10:09:13 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

Test code:


import (
    "fmt"
    "sync"
    "time"

"github.com/Workiva/go-datastructures/queue"


)

var wg sync.WaitGroup

func main() {
    wg.Add(2)
    q := queue.NewRingBuffer(4096)

go func() {
defer wg.Done()
for {
time.Sleep(3 * time.Second)
q.Offer("hello world")
}
}()

go func() {
defer wg.Done()
for {
str, _ := q.Get()
fmt.Println(str)
}
}()

wg.Wait()


}

$go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/sheldon/Gopath"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GO15VENDOREXPERIMENT="1"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"

$go version
go version go1.6.2 linux/amd64

Im seeing the same. (Go 1.8)

package main

import (
  "github.com/Workiva/go-datastructures/queue"
)

func main() {
  rb := queue.NewRingBuffer(5)
  rb.Get()
}

100% cpu usage for above

100% CPU

any news for this?

I need do this:

for rb.Len() == 0 { time.Sleep(50 * time.MillSecond) }
item, err := rb.Get()

another patch use chan notification, run better now

diff --git a/queue/ring.go b/queue/ring.go
index dfd0b53..8b40dc9 100644
--- a/queue/ring.go
+++ b/queue/ring.go
@@ -57,6 +57,7 @@ type RingBuffer struct {
 	_padding2      [8]uint64
 	mask, disposed uint64
 	_padding3      [8]uint64
+	condC          chan bool
 	nodes          nodes
 }
 
@@ -67,6 +68,7 @@ func (rb *RingBuffer) init(size uint64) {
 		rb.nodes[i] = &node{position: i}
 	}
 	rb.mask = size - 1 // so we don't have to do this with every put/get operation
+	rb.condC = make(chan bool, 0)
 }
 
 // Put adds the provided item to the queue.  If the queue is full, this
@@ -115,6 +117,12 @@ L:
 
 	n.data = item
 	atomic.StoreUint64(&n.position, pos+1)
+	if rb.Len() < 3 {
+		select {
+		case rb.condC <- true:
+		default:
+		}
+	}
 	return true, nil
 }
 
@@ -146,6 +154,12 @@ L:
 			return nil, ErrDisposed
 		}
 
+		if rb.Len() == 0 {
+			select {
+			case <-rb.condC:
+			}
+		}
+
 		n = rb.nodes[pos&rb.mask]
 		seq := atomic.LoadUint64(&n.position)
 		switch dif := seq - (pos + 1); {
@@ -186,6 +200,10 @@ func (rb *RingBuffer) Cap() uint64 {
 // queue will return an error.
 func (rb *RingBuffer) Dispose() {
 	atomic.CompareAndSwapUint64(&rb.disposed, 0, 1)
+	select {
+	case rb.condC <- true:
+	default:
+	}
 }
 
 // IsDisposed will return a bool indicating if this queue has been

Get() has 100% CPU usage because it uses busy waiting strategy when the buffer is empty.

Ideally we should add a new method which returns as soon as we find out the buffer is empty, and returns a boolean to allow users to decide what they want to do in this case (busy waiting, sleep, or something else). This is we do in Put() and Offer().

For now users can workaround this issue by replacing Get() with Poll(1), which returns (nil, ErrTimeout) after the buffer has been empty for 1ns.