justinstenning / SharedMemory

C# shared memory classes for sharing data between processes (Array, Buffer and Circular Buffer)
https://www.nuget.org/packages/SharedMemory
Other
566 stars 118 forks source link

Keep nodes readable in CircularBuffer until all readers have read the node #8

Open justinstenning opened 8 years ago

justinstenning commented 8 years ago

Allow all consumers of a circular buffer to retrieve all the data, i.e. the nodes are not writable again until all consumers have read the data.

One of the ideas around how to do this was to use an array of ReadStart pointers in the nodeheader rather than a single pointer. But probably also using a counter for the DoneReading would be needed...

MikeFair commented 8 years ago

Given it's a circularly linked list; I was thinking a writer could insert new nodes (or a new block of nodes) dynamically; like an arrayList segment. However one of my concerns is readers/consumers that don't do their part and read the messages.

What usually happens when a reader doesn't consume messages is either A) The queue fills up and additional messages are dropped because there's no space, or B) The queue fills up and the older messages on the queue are expired/dropped.

Option (B) is what a circular linked list could normally do I think; as the tail wrapped around, it would just start overwriting the head of the list; theoretically it should push the ReadStart pointer forward as it goes. This way when/if the reader starts reading, it will start consuming whatever messages haven't been overwritten yet. Perhaps a "messages dropped" counter on the readPointer that gets incremented whenever the readpointer gets advanced this way?

I'm not sure how I see (A) happening. So far the only ways I see to isolate the problem to just the readers that aren't getting messages involves allocating more memory; either the consumer allocated it so the writer can copy the data into the private area (like by providing the name of another memory file (or memory region) where the data can get copied as it is overwritten), and if that area gets full too then additional messages would get dropped for that reader. Or the circular buffer itself uses a new memory area (allocates a new Segment like in an ArrayList), and leaves the old memory area behind for those that still have yet to read those messages but links the head/tail of the old area into the new so that the reader will be able to follow the chain... My concern here is that a hung or blocked process would cause a runaway memory consumption issue.

All in all I think Option B is simpler while maximizing the potential for readers to get all the messages in the right order but at somewhat different speeds.

justinstenning commented 8 years ago

Currently the writer will wait indefinitely, i.e. no readers read the node, the producer fills the buffer then waits for a free node.

Due to the increasing complexity I'm starting to think it would make sense for these additional options (including #7 perhaps) to be in new purpose built classes. I'll have a think about how to structure the CircularBuffer class to more easily support this.

MikeFair commented 8 years ago

Agreed; I'm thinking of this higher level object as like an IPC network switch. That object then maintains a list of circular node lists; one list per producer/consumer object on the "switch" (aka attached to the file). Creating a new producer/consumer object would simply add a new circular buffer to the list.

Every write would first add the data node to the backplane (aka add it to the "switch" master array buffer).

Then it would iterate through the list of connected circular buffers and insert a pointer to that data node on each connected child queue. These queues can fill up and will get skipped if there's no room in their queue (aka they missed the packet).

I think there's also a simple and safe way to safely detect when a dataNode can be reused, by creating a second circularly linked list. Starting with that dataNode as the head node, then linking to each of child queue's node pointer reference nodes; as each child queue reads the data node it would update its neighbor's previous and next pointers to remove itself from the dataNode's child queue chain. When a child queue Read event detects that its own previous and next links are pointing to the same place, and the same place as the dataNode itself; it knows that it's the last reader and can mark the dataNode as read.

The switch itself then just needs to find an open/read dataNode to use whenever a new data item gets written. It can treat the array as a circular buffer, iterating from the last written index and advancing forward until it finds an open node.

On Feb 3, 2016 1:43 AM, "Justin Stenning" notifications@github.com wrote:

Currently the writer will wait indefinitely, i.e. no readers read the node, the producer fills the buffer then waits for a free node.

Due to the increasing complexity I'm starting to think it would make sense for these additional options (including #7 https://github.com/spazzarama/SharedMemory/issues/7 perhaps) to be in new purpose built classes. I'll have a think about how to structure the CircularBuffer class to more easily support this.

— Reply to this email directly or view it on GitHub https://github.com/spazzarama/SharedMemory/issues/8#issuecomment-179127458 .