-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bugfix // Fix race condition in Pop
& PopN
operation of ring buffer
#177
base: master
Are you sure you want to change the base?
Bugfix // Fix race condition in Pop
& PopN
operation of ring buffer
#177
Conversation
pop
operation of ring bufferPop
& PopN
operation of ring buffer
@@ -61,6 +61,11 @@ func (rb *RingBuffer[T]) Pop() (T, bool) { | |||
return t, false | |||
} | |||
rb.mu.Lock() | |||
if rb.len == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we check for the ringbuffer length twice here, 1st time using rb.Len(), and second time using rb.len
func (rb *RingBuffer[T]) Pop() (T, bool) {
rb.mu.Lock() // lock here to avoid reading length 2 times
if rb.Len() == 0 {
rb.mu.Unlock()
var t T
return t, false
}
rb.content.head = (rb.content.head + 1) % rb.content.mod
item := rb.content.items[rb.content.head]
var t T
rb.content.items[rb.content.head] = t
atomic.AddInt64(&rb.len, -1)
rb.mu.Unlock()
return item, true
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's called double-checked locking pattern. There are usually two options we can use:
- Lock immediately:
rb.mu.Lock()
if rb.len == 0 {
rb.mu.Unlock()
var t T
return t, false
}
- Use double-checked locking:
if rb.Len() == 0 { // As far as I understand, we read atomically to prevent tearing read of int64.
var t T
return t, false
}
rb.mu.Lock()
if rb.len == 0 {
rb.mu.Unlock()
var t T
return t, false
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I would prefer the first option, since it's bit more readable and we do not need to check the rb length twice.
Good job for detecting the issue!
@@ -75,6 +80,10 @@ func (rb *RingBuffer[T]) PopN(n int64) ([]T, bool) { | |||
return nil, false | |||
} | |||
rb.mu.Lock() | |||
if rb.len == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing here:
func (rb *RingBuffer[T]) PopN(n int64) ([]T, bool) {
rb.mu.Lock()
if rb.Len() == 0 {
rb.mu.Unlock()
return nil, false
}
content := rb.content
if n >= rb.len {
n = rb.len
}
atomic.AddInt64(&rb.len, -n)
items := make([]T, n)
for i := int64(0); i < n; i++ {
pos := (content.head + 1 + i) % content.mod
items[i] = content.items[pos]
var t T
content.items[pos] = t
}
content.head = (content.head + n) % content.mod
rb.mu.Unlock()
return items, true
}
@mapogolions I just checked the updates. It's better to use atomic operation |
@tprifti |
You are technically correct. I suggested using atomic read so we can be consistent across the repo. Also, it would be helpful for other contributors to notice that reading the length of ringbuffer needs to be atomic. |
The current implementation of the ring buffer has a thread-safety issue in the
Pop
&PopN
operation.Attempting to remove an element may lead to a race condition, resulting in the ring buffer being left in an invalid state after calling
Pop
. Similarly, in the case ofPopN
, the method might incorrectly indicate that elements were removed, even though no elements were actually removed because the ring buffer was empty.To illustrate the problem, I have added unit tests that can reproduce the issue. Feel free to delete these tests once you have verified the existence of the bug, as including such tests might be questionable, given that they expose a non-deterministic problem.