C++ bindings for libpmemobj (part 3) - persistent queue example
The best way to learn to code is usually by implementing an example. We are going
to be creating a linked-list based queue data structure using the the
pmem::obj::p
and pmem::obj::persistent_ptr
classes and libpmemobj C API. But
first, a little bit of CS 101 :)
Linked-list queue
Queue is a collection of elements with two important operations:
push
- adds element to thetail
of the structurepop
- removes element from thehead
of the structure
This makes the queue a First-In-First-Out (FIFO) data structure.
The common implementations use either arrays or singly linked-lists to store elements. As previously stated, we are going to use the latter.
pmem_queue::push()
Step 1
Push operation consists of several steps. First, a new object is created that
contains the data (value
) and a pointer to the next
entry. Initially the
memory content of those variables is unknown, so we need to assign user input
to the value
and NULL
to the next
.
Step 2
After we’ve done that, let’s link the newly created object to the structure
by assigning it to the next
field of the current tail entry. Obviously, if the
tail is NULL
there’s nothing to do :)
Step 3
As the last step, we have to update the tail
variable of the queue structure to
point to our new object. If this is the first entry and both head
and tail
are NULL
,
we have to remember to update the head
as well.
pmem_queue::pop()
Step 1
The first step of the pop operation is to update the head
variable of the queue
structure to point to the head->next
(the second entry). The object which is
being removed is currently not linked to anything.
Step 2
Next, the removed object has to be freed. And if there are no more entries in
the queue (the head
variable is NULL
) the tail
variable has to be set to NULL
.
What about persistence?
When implementing algorithms that operate on persistent memory, we have to carefully consider what happens to the data structure if the application is interrupted (for example, by a power failure) at any moment during its operation.
The pmemobj library takes care of those issues and it’s enough to simply wrap your code in a transaction block to make it failsafe atomic (i.e. done completely or not at all).
I intentionally described the queue operations in indivisible steps, so that it’s clear that when the algorithm is interrupted for some reason, we can end up with either persistent memory leak or corrupted data structure.
C++ implementation
As the operations were already described, and the actual implementation, in alignment to the premise of C++ bindings, is identical to the volatile one, I’ll talk only about the data structure.
First let’s define our list entry structure. As we just learned, it has to
contain a next
pointer and a value.
struct pmem_entry {
persistent_ptr<pmem_entry> next;
p<uint64_t> value;
};
Our queue structure, which will also be our root object, consists of two fields:
head
and tail
:
class pmem_queue {
private:
persistent_ptr<pmem_entry> head;
persistent_ptr<pmem_entry> tail;
};
The pmem_queue::push()
and pmem_queue::pop()
functions look exactly like a
volatile implementation, but with a transaction block and
pmemobj_alloc()
/pmemobj_free()
instead of new
/delete
.
Complete implementation is available here.