Documentation
¶
Overview ¶
Package triplebuffer exposes a potentially viable triple-buffer system for managing data between a producer and consumer where avoiding read/write deadlocks might be problematic. Don't trust anything in this package for Production (prod) usage, oh my god.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Buffer ¶
type Buffer[T any] struct { // contains filtered or unexported fields }
Buffer is a triple buffer! Enjoy!
A Buffer has three (private) fields, to wit:
- back: the Back buffer, which is whatever a producer is working on. This is a pointer, so it is modifiable after setting without any extra work. Initially set by Write, then promoted via atomic swap to the Middle buffer by calling Commit after the producer has finished whatever it needs to do with the data.
- middle: the Middle buffer, also known as the very special atomic buffer. Data in the Middle buffer can be in one of two states: "pending", or "stale". Pending data is that which has been committed (via Commit), and is considered new data. When Read is next called, the new data will be promoted to the Front buffer, with the previous Front buffer data being atomically swapped into the Middle buffer and marked as "stale". Stale data has been consumed already.
- front: the Front buffer. Whatever is in here will be returned by Read if there is no pending data in the Middle buffer. If there is pending data in Middle, that data gets promoted and returned by Read.
func NewBuffer ¶
NewBuffer creates a new empty Buffer.
Example ¶
Example of instantiating an empty buffer and writing to it every 750 milliseconds. Thanks to the timer in produce(), reads will rarely be brand new pending reads, but sometimes they can be if the stars align.
package main
import (
"fmt"
"time"
"github.com/jalavosus/triplebuffer"
)
func main() {
const maxProduceInt int = 42
produce := func(buf *triplebuffer.Buffer[int]) {
ticker := time.NewTicker(750 * time.Millisecond)
for i := 0; i <= maxProduceInt; i++ {
buf.Write(&i)
buf.Commit()
<-ticker.C
}
}
buf := triplebuffer.NewBuffer[int]()
go produce(buf)
var prevRead int
for {
val, pending := buf.Read()
if val != nil && *val != prevRead {
dVal := *val
fmt.Printf("val: %d - pending %t\n", dVal, pending)
if dVal >= maxProduceInt {
break
}
prevRead = dVal
}
}
}
Output:
func NewBufferWithFront ¶
NewBufferWithFront returns a Buffer with the Front buffer populated, and ready to be consumed via Buffer.Read.
func NewPopulatedBuffer ¶
NewPopulatedBuffer returns a fully populated Buffer, or as populated as the caller makes it, as nil is a valid value for all three params.
Note: the Middle buffer item (`middle`) will be considered Pending unless it is nil.
func (*Buffer[T]) Commit ¶
func (b *Buffer[T]) Commit()
Commit is part of the producer API.
It is called by the producer when it has finished working on (producing) data which is in the Back buffer, and is ready to be moved to the Middle for consumption.
On commit, the following happens:
- Back buffer data is promoted to the Middle buffer by atomic swap.
- Middle buffer data is moved into the Back buffer.
Note that Commit can (and, likely at some point *will*) overwrite whatever is in the Middle buffer, even if that data is still pending.
func (*Buffer[T]) Read ¶
Read is part of the consumer API. It returns the value currently in the Front buffer.
If the value currently in the Middle buffer is pending, (it has just been commited by a producer) then the Front and Middle buffers will be swapped and the *new* Front buffer returned. Otherwise, the stale Front buffer value will be returned.
If there is no value in the Middle buffer, no swap occurs. The returned boolean corresponds to whether the value is
- "stale": false
- "pending": true.