For an LRU cache I need an algorithm for a lock-free queue similar to the one described in the paper Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms
But to maintain an LRU queue I will also need the possibility to delete nodes within the queue (except for nodes at the tail).
My idea is to just mark nodes as deleted with a CAS operation and then use a single cleanup thread which will later on remove the deleted nodes from within the queue.
I found that creation of lock-free algorithm is more complicated than I first anticipated. So, my question is:
Is there any such algorithm available already?
This is the structure that I use currently:
General
- A node has the following structure: struct { e *entry, next pointer, prev pointer, state uint32}
- To avoid multiple memory allocation for each new node an array of nodes are allocated.
- A pointer to a node consists of the array index value and a update counter, multiplexed into a single uint64
- A node state consist of a high bit telling if the node is deleted or not. Rest of the bits are used as update counter
Enqueue
- An auxiliary queue holds a list of unused nodes and a "new" node is fetched through dequeue from the aux queue and then set to default
- node.prev is set to current tail prior to the new node being enqueued
Dequeue
- head.next.prev is CAS'ed to NIL value prior to head dequeue. In case head.next.prev is set to CLEANUP (being processed by the cleanup thread), a dequeue is not allowed and the thread will yield the CPU and start over again.
- On successful dequeue of a node with an undeleted state the state will be CAS'ed to deleted and the node will be returned to the auxilary queue. On failure (or state already set to deleted), the dequeued node.prev will be changed from NIL to CLEANUP, signaling to the cleanup thread that the node is dequeued. The dequeue will then start over until an undeleted node is successfully dequeued and CAS'ed to deleted.
Delete
- To mark in-queue deletion, state is CAS'ed to deleted and passed to a cleanup queue on success. Nothing is done on failure, but the function returns.
Cleanup thread
- If node.prev is CLEANUP, a Dequeue has removed it from queue. Node is passed back to auxilary queue.
- If node.prev is NIL, the node is about to become head, is head, or is about to be dequeued. If node == head, cleanup thread tries to perform a dequeue (with state changed to deleted). On failure the cleanup process starts all over.
- If node is set to another node, node.prev is CAS'ed to CLEANUP. This prevents any dequeue to be made if as soon as head.next == node. On success, in-queue removal is made using the double-linked list. On failure the cleanup process starts all over.
Node.prev is used to tell:
- What node is previous in the queue
- Node is about to become head/is head
- Node is being processed by the cleanup thread
- Node is dequeued