Cog Audio: Improve virtual ring buffer class
parent
a76f3c3476
commit
3b125c0440
|
@ -44,7 +44,7 @@
|
||||||
// However, if you have multiple reader or writer threads, all bets are off!
|
// However, if you have multiple reader or writer threads, all bets are off!
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <stdatomic.h>
|
||||||
|
|
||||||
@interface VirtualRingBuffer : NSObject
|
@interface VirtualRingBuffer : NSObject
|
||||||
{
|
{
|
||||||
|
@ -55,8 +55,9 @@
|
||||||
// bufferEnd is the end of the "real" buffer (always buffer + bufferLength).
|
// bufferEnd is the end of the "real" buffer (always buffer + bufferLength).
|
||||||
// Note that the "virtual" portion of the buffer extends from bufferEnd to bufferEnd+bufferLength.
|
// Note that the "virtual" portion of the buffer extends from bufferEnd to bufferEnd+bufferLength.
|
||||||
|
|
||||||
void *readPointer;
|
atomic_int readPointer;
|
||||||
void *writePointer;
|
atomic_int writePointer;
|
||||||
|
atomic_int bufferFilled;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithLength:(UInt32)length;
|
- (id)initWithLength:(UInt32)length;
|
||||||
|
|
|
@ -44,8 +44,9 @@ static void deallocateVirtualBuffer(void *buffer, UInt32 bufferLength);
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
readPointer = NULL;
|
atomic_init(&readPointer, 0);
|
||||||
writePointer = NULL;
|
atomic_init(&writePointer, 0);
|
||||||
|
atomic_init(&bufferFilled, 0);
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
@ -61,13 +62,14 @@ static void deallocateVirtualBuffer(void *buffer, UInt32 bufferLength);
|
||||||
// Assumption:
|
// Assumption:
|
||||||
// No one is reading or writing from the buffer, in any thread, when this method is called.
|
// No one is reading or writing from the buffer, in any thread, when this method is called.
|
||||||
|
|
||||||
readPointer = NULL;
|
atomic_init(&readPointer, 0);
|
||||||
writePointer = NULL;
|
atomic_init(&writePointer, 0);
|
||||||
|
atomic_init(&bufferFilled, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)isEmpty
|
- (BOOL)isEmpty
|
||||||
{
|
{
|
||||||
return (readPointer == NULL && writePointer == NULL);
|
return (atomic_load_explicit(&bufferFilled, memory_order_relaxed) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -98,21 +100,23 @@ static void deallocateVirtualBuffer(void *buffer, UInt32 bufferLength);
|
||||||
|
|
||||||
UInt32 length;
|
UInt32 length;
|
||||||
// Read this pointer exactly once, so we're safe in case it is changed in another thread
|
// Read this pointer exactly once, so we're safe in case it is changed in another thread
|
||||||
void *localWritePointer = writePointer;
|
int localWritePointer = atomic_load_explicit(&writePointer, memory_order_relaxed);
|
||||||
|
int localReadPointer = atomic_load_explicit(&readPointer, memory_order_relaxed);
|
||||||
|
int localBufferFilled = atomic_load_explicit(&bufferFilled, memory_order_relaxed);
|
||||||
|
|
||||||
// Depending on out-of-order execution and memory storage, either one of these may be NULL when the buffer is empty. So we must check both.
|
// Depending on out-of-order execution and memory storage, either one of these may be NULL when the buffer is empty. So we must check both.
|
||||||
if (!readPointer || !localWritePointer) {
|
if (localReadPointer == localWritePointer && !localBufferFilled) {
|
||||||
// The buffer is empty
|
// The buffer is empty
|
||||||
length = 0;
|
length = 0;
|
||||||
} else if (localWritePointer > readPointer) {
|
} else if (localWritePointer > localReadPointer) {
|
||||||
// Write is ahead of read in the buffer
|
// Write is ahead of read in the buffer
|
||||||
length = (UInt32)(localWritePointer - readPointer);
|
length = (UInt32)(localWritePointer - localReadPointer);
|
||||||
} else {
|
} else {
|
||||||
// Write has wrapped around past read, OR write == read (the buffer is full)
|
// Write has wrapped around past read, OR write == read (the buffer is full)
|
||||||
length = (UInt32)(bufferLength - (readPointer - localWritePointer));
|
length = (UInt32)(bufferLength - localReadPointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
*returnedReadPointer = readPointer;
|
*returnedReadPointer = buffer + localReadPointer;
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,25 +126,10 @@ static void deallocateVirtualBuffer(void *buffer, UInt32 bufferLength);
|
||||||
// [self lengthAvailableToReadReturningPointer:] currently returns a value >= length
|
// [self lengthAvailableToReadReturningPointer:] currently returns a value >= length
|
||||||
// length > 0
|
// length > 0
|
||||||
|
|
||||||
void *newReadPointer;
|
if (atomic_fetch_add(&readPointer, length) + length >= bufferLength)
|
||||||
|
atomic_fetch_sub(&readPointer, bufferLength);
|
||||||
|
|
||||||
newReadPointer = readPointer + length;
|
atomic_fetch_sub(&bufferFilled, length);
|
||||||
|
|
||||||
if (newReadPointer == (void *)(intptr_t) length) {
|
|
||||||
// Someone somehow read from us before another thread emptied the buffer
|
|
||||||
newReadPointer = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newReadPointer >= bufferEnd)
|
|
||||||
newReadPointer -= bufferLength;
|
|
||||||
|
|
||||||
if (newReadPointer == writePointer) {
|
|
||||||
// We just read the last data out of the buffer, so it is now empty.
|
|
||||||
newReadPointer = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the new read pointer. This is the only place this happens in the read thread.
|
|
||||||
readPointer = newReadPointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,23 +144,27 @@ static void deallocateVirtualBuffer(void *buffer, UInt32 bufferLength);
|
||||||
|
|
||||||
UInt32 length;
|
UInt32 length;
|
||||||
// Read this pointer exactly once, so we're safe in case it is changed in another thread
|
// Read this pointer exactly once, so we're safe in case it is changed in another thread
|
||||||
void *localReadPointer = readPointer;
|
int localReadPointer = atomic_load_explicit(&readPointer, memory_order_relaxed);
|
||||||
|
int localWritePointer = atomic_load_explicit(&writePointer, memory_order_relaxed);
|
||||||
|
int localBufferFilled = atomic_load_explicit(&bufferFilled, memory_order_relaxed);
|
||||||
|
|
||||||
// Either one of these may be NULL when the buffer is empty. So we must check both.
|
// Either one of these may be NULL when the buffer is empty. So we must check both.
|
||||||
if (!localReadPointer || !writePointer) {
|
if (!localReadPointer && !localWritePointer && !localBufferFilled) {
|
||||||
// The buffer is empty. Set it up to be written into.
|
// The buffer is empty. Set it up to be written into.
|
||||||
// This is one of the two places the write pointer can change; both are in the write thread.
|
// This is one of the two places the write pointer can change; both are in the write thread.
|
||||||
writePointer = buffer;
|
|
||||||
length = bufferLength;
|
length = bufferLength;
|
||||||
} else if (writePointer <= localReadPointer) {
|
} else if (localWritePointer <= localReadPointer) {
|
||||||
// Write is before read in the buffer, OR write == read (meaning that the buffer is full).
|
// Write is before read in the buffer, OR write == read (meaning that the buffer is full).
|
||||||
length = (UInt32)(localReadPointer - writePointer);
|
length = (UInt32)(localReadPointer - localWritePointer);
|
||||||
|
if (!length && !localBufferFilled) {
|
||||||
|
length = (UInt32)(bufferLength - localWritePointer);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Write is behind read in the buffer. The available space wraps around.
|
// Write is behind read in the buffer. The available space wraps around.
|
||||||
length = (UInt32)((bufferEnd - writePointer) + (localReadPointer - buffer));
|
length = (UInt32)(bufferLength - localWritePointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
*returnedWritePointer = writePointer;
|
*returnedWritePointer = buffer + localWritePointer;
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,22 +174,10 @@ static void deallocateVirtualBuffer(void *buffer, UInt32 bufferLength);
|
||||||
// [self lengthAvailableToWriteReturningPointer:] currently returns a value >= length
|
// [self lengthAvailableToWriteReturningPointer:] currently returns a value >= length
|
||||||
// length > 0
|
// length > 0
|
||||||
|
|
||||||
void *oldWritePointer = writePointer;
|
if (atomic_fetch_add(&writePointer, length) + length >= bufferLength)
|
||||||
void *newWritePointer;
|
atomic_fetch_sub(&writePointer, bufferLength);
|
||||||
|
|
||||||
// Advance the write pointer, wrapping around if necessary.
|
atomic_fetch_add(&bufferFilled, length);
|
||||||
newWritePointer = writePointer + length;
|
|
||||||
if (newWritePointer >= bufferEnd)
|
|
||||||
newWritePointer -= bufferLength;
|
|
||||||
|
|
||||||
// This is one of the two places the write pointer can change; both are in the write thread.
|
|
||||||
writePointer = newWritePointer;
|
|
||||||
|
|
||||||
// Also, if the read pointer is NULL, then we just wrote into a previously empty buffer, so set the read pointer.
|
|
||||||
// This is the only place the read pointer is changed in the write thread.
|
|
||||||
// The read thread should never change the read pointer when it is NULL, so this is safe.
|
|
||||||
if (!readPointer)
|
|
||||||
readPointer = oldWritePointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
Loading…
Reference in New Issue