memgraph/include/data_structures/concurrent/skiplist.hpp
2016-08-10 09:39:02 +01:00

882 lines
26 KiB
C++

#pragma once
#include <algorithm>
#include <cassert>
#include <memory>
#include "utils/placeholder.hpp"
#include "utils/random/fast_binomial.hpp"
#include "threading/sync/lockable.hpp"
#include "threading/sync/spinlock.hpp"
#include "data_structures/concurrent/skiplist_gc.hpp"
/* @brief Concurrent lock-based skiplist with fine grained locking
*
* From Wikipedia:
* "A skip list is a data structure that allows fast search within an
* ordered sequence of elements. Fast search is made possible by
* maintaining a linked hierarchy of subsequences, each skipping over
* fewer elements. Searching starts in the sparsest subsequence until
* two consecutive elements have been found, one smaller and one
* larger than or equal to the element searched for."
*
* [_]---------------->[+]----------->[_]
* [_]->[+]----------->[+]------>[+]->[_]
* [_]->[+]------>[+]->[+]------>[+]->[_]
* [_]->[+]->[+]->[+]->[+]->[+]->[+]->[_]
* head 1 2 4 5 8 9 nil
*
* The logarithmic properties are maintained by randomizing the height for
* every new node using the binomial distribution
* p(k) = (1/2)^k for k in [1...H].
*
* The implementation is based on the work described in the paper
* "A Provably Correct Scalable Concurrent Skip List"
* URL: https://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/OPODIS2006-BA.pdf
*
* The proposed implementation is in Java so the authors don't worry about
* garbage collection, but obviously we have to. This implementation uses
* lazy garbage collection. When all clients stop using the skiplist, we can
* be sure that all logically removed nodes are not visible to anyone so
* we can safely remove them. The idea of counting active clients implies
* the use of a intermediary structure (called Accessor) when accessing the
* skiplist.
*
* The implementation has an interface which closely resembles the functions
* with arguments and returned types frequently used by the STL.
*
* Example usage:
* Skiplist<T> skiplist;
*
* {
* auto accessor = skiplist.access();
*
* // inserts item into the skiplist and returns
* // <iterator, bool> pair. iterator points to the newly created
* // node and the boolean member evaluates to true denoting that the
* // insertion was successful
* accessor.insert(item1);
*
* // nothing gets inserted because item1 already exist in the skiplist
* // returned iterator points to the existing element and the return
* // boolean evaluates to false denoting the failed insertion
* accessor.insert(item1);
*
* // returns an iterator to the element item1
* auto it = accessor.find(item1);
*
* // returns an empty iterator. it == accessor.end()
* auto it = accessor.find(item2);
*
* // iterate over all items
* for(auto it = accessor.begin(); it != accessor.end(); ++it)
* cout << *it << endl;
*
* // range based for loops also work
* for(auto& e : accessor)
* cout << e << endl;
*
* accessor.remove(item1); // returns true
* accessor.remove(item1); // returns false because key1 doesn't exist
* }
*
* // accessor out of scope, garbage collection might occur
*
* For detailed operations available, please refer to the Accessor class
* inside the public section of the SkipList class.
*
* @tparam T Type to use as the item
* @tparam H Maximum node height. Determines the effective number of nodes
* the skiplist can hold in order to preserve it's log2 properties
* @tparam lock_t Lock type used when locking is needed during the creation
* and deletion of nodes.
*/
template <class T, size_t H = 32, class lock_t = SpinLock>
class SkipList : private Lockable<lock_t>
{
public:
// computes the height for the new node from the interval [1...H]
// with p(k) = (1/2)^k for all k from the interval
static thread_local FastBinomial<H> rnd;
/* @brief Wrapper class for flags used in the implementation
*
* MARKED flag is used to logically delete a node.
* FULLY_LINKED is used to mark the node as fully inserted, i.e. linked
* at all layers in the skiplist up to the node height
*/
struct Flags
{
enum node_flags : uint8_t
{
MARKED = 0x01,
FULLY_LINKED = 0x10,
};
bool is_marked() const { return flags.load() & MARKED; }
void set_marked() { flags.fetch_or(MARKED); }
bool is_fully_linked() const { return flags.load() & FULLY_LINKED; }
void set_fully_linked() { flags.fetch_or(FULLY_LINKED); }
private:
std::atomic<uint8_t> flags{0};
};
class Node : Lockable<lock_t>
{
public:
friend class SkipList;
const uint8_t height;
Flags flags;
T &value() { return data.get(); }
const T &value() const { return data.get(); }
static Node *sentinel(uint8_t height)
{
// we have raw memory and we need to construct an object
// of type Node on it
return new (allocate(height)) Node(height);
}
static Node *create(const T &item, uint8_t height)
{
return create(item, height);
}
static Node *create(T &&item, uint8_t height)
{
auto node = allocate(height);
// we have raw memory and we need to construct an object
// of type Node on it
return new (node) Node(std::forward<T>(item), height);
}
static void destroy(Node *node)
{
node->~Node();
std::free(node);
}
Node *forward(size_t level) const { return tower[level].load(); }
void forward(size_t level, Node *next) { tower[level].store(next); }
private:
Node(uint8_t height) : height(height)
{
// here we assume, that the memory for N towers (N = height) has
// been allocated right after the Node structure so we need to
// initialize that memory
for (auto i = 0; i < height; ++i)
new (&tower[i]) std::atomic<Node *>{nullptr};
}
Node(T &&data, uint8_t height) : Node(height)
{
this->data.set(std::forward<T>(data));
}
~Node()
{
for (auto i = 0; i < height; ++i)
tower[i].~atomic();
}
static Node *allocate(uint8_t height)
{
// [ Node ][Node*][Node*][Node*]...[Node*]
// | | | | |
// | 0 1 2 height-1
// |----------------||-----------------------------|
// space for Node space for tower pointers
// structure right after the Node
// structure
auto size = sizeof(Node) + height * sizeof(std::atomic<Node *>);
auto node = static_cast<Node *>(std::malloc(size));
return node;
}
Placeholder<T> data;
// this creates an array of the size zero. we can't put any sensible
// value here since we don't know what size it will be untill the
// node is allocated. we could make it a Node** but then we would
// have two memory allocations, one for node and one for the forward
// list. this way we avoid expensive malloc/free calls and also cache
// thrashing when following a pointer on the heap
std::atomic<Node *> tower[0];
};
public:
template <class It>
class IteratorBase : public Crtp<It>
{
protected:
IteratorBase(Node *node) : node(node) {}
Node *node{nullptr};
public:
IteratorBase() = default;
IteratorBase(const IteratorBase &) = default;
T &operator*()
{
assert(node != nullptr);
return node->value();
}
T *operator->()
{
assert(node != nullptr);
return &node->value();
}
operator T &()
{
assert(node != nullptr);
return node->value();
}
It &operator++()
{
assert(node != nullptr);
node = node->forward(0);
return this->derived();
}
bool has_next()
{
assert(node != nullptr);
return node->forward(0) != nullptr;
}
It &operator++(int) { return operator++(); }
friend bool operator==(const It &a, const It &b)
{
return a.node == b.node;
}
friend bool operator!=(const It &a, const It &b) { return !(a == b); }
};
class ConstIterator : public IteratorBase<ConstIterator>
{
friend class SkipList;
ConstIterator(Node *node) : IteratorBase<ConstIterator>(node) {}
public:
ConstIterator() = default;
ConstIterator(const ConstIterator &) = default;
const T &operator*()
{
return IteratorBase<ConstIterator>::operator*();
}
const T *operator->()
{
return IteratorBase<ConstIterator>::operator->();
}
operator const T &()
{
return IteratorBase<ConstIterator>::operator T &();
}
};
class Iterator : public IteratorBase<Iterator>
{
friend class SkipList;
Iterator(Node *node) : IteratorBase<Iterator>(node) {}
public:
Iterator() = default;
Iterator(const Iterator &) = default;
};
template <class K>
class MultiIterator : public Crtp<MultiIterator<K>>
{
friend class SkipList;
MultiIterator(const K &data) : data(data), skiplist(nullptr)
{
succs[0] = nullptr;
};
MultiIterator(SkipList *skiplist, const K &data)
: data(data), skiplist(skiplist)
{
while (true) {
auto level = find_path(skiplist, H - 1, data, preds, succs);
if (level == -1) {
succs[0] = nullptr;
} else if (succs[0] != succs[succs[0]->height - 1] ||
!succs[level]->flags.is_fully_linked()) {
usleep(250);
continue;
}
break;
}
}
public:
MultiIterator(const MultiIterator &) = default;
T &operator*()
{
assert(succs[0] != nullptr);
return succs[0]->value();
}
T *operator->()
{
assert(succs[0] != nullptr);
return &succs[0]->value();
}
operator T &()
{
assert(succs[0] != nullptr);
return succs[0]->value();
}
bool has_next()
{
assert(succs[0] != nullptr);
return succs[0].forward(0) != nullptr;
}
bool has_value() { return succs[0] != nullptr; }
MultiIterator &operator++()
{
assert(succs[0] != nullptr);
// This whole method can be optimized if it's valid to expect height
// of 1 on same key elements.
for (int i = succs[0]->height - 1; i >= 0; i--) {
preds[i] = succs[i];
succs[i] = preds[i]->forward(i);
}
if (succs[0] != nullptr) {
if (succs[0]->value() != data) {
succs[0] = nullptr;
} else {
while (succs[0] != succs[succs[0]->height - 1] ||
!succs[0]->flags.is_fully_linked()) {
usleep(250);
for (int i = succs[0]->height - 1; i >= 0; i--) {
succs[i] = preds[i]->forward(i);
}
}
}
}
return this->derived();
}
MultiIterator &operator++(int) { return operator++(); }
friend bool operator==(const MultiIterator &a, const MultiIterator &b)
{
return a.succs[0] == b.succs[0];
}
friend bool operator!=(const MultiIterator &a, const MultiIterator &b)
{
return !(a == b);
}
bool is_removed()
{
assert(succs[0] != nullptr);
return succs[0]->flags.is_marked();
}
// True if this call successfuly removed value. ITERATOR IS'T ADVANCED.
// False may mean that data has already been removed.
bool remove()
{
assert(succs[0] != nullptr);
return skiplist->template remove<K>(
data, preds, succs,
SkipList<T>::template MultiIterator<K>::update_path);
}
private:
static int update_path(SkipList *skiplist, int start, const K &item,
Node *preds[], Node *succs[])
{
// One optimization here would be to wait for is_fully_linked to be
// true. That way that doesnt have to be done in constructor and
// ++ operator.
int level_found = succs[0]->height - 1;
assert(succs[0] == succs[level_found]);
// for (int i = level_found; i >= 0; i--) {
// // Someone has done something
// if (preds[i]->forward(i) != succs[i]) {
for (auto it = MultiIterator<K>(skiplist, item); it.has_value();
it++) {
if (it.succs[0] == succs[0]) { // Found it
std::copy(it.preds, it.preds + H, preds);
std::copy(it.succs, it.succs + H, succs);
return level_found;
}
}
// Someone removed it
// assert(succs[0]->flags.is_marked());
return -1;
// }
// }
// // Everything is fine
// return level_found;
}
const K &data;
SkipList *skiplist;
Node *preds[H], *succs[H];
};
SkipList() : header(Node::sentinel(H)) {}
~SkipList()
{
// Someone could be using this map through an Accessor.
Node *now = header;
header = nullptr;
while (now != nullptr) {
Node *next = now->forward(0);
Node::destroy(now);
now = next;
}
}
friend class Accessor;
class Accessor
{
friend class SkipList;
Accessor(SkipList *skiplist) : skiplist(skiplist)
{
assert(skiplist != nullptr);
skiplist->gc.add_ref();
}
public:
Accessor(const Accessor &) = delete;
Accessor(Accessor &&other) : skiplist(other.skiplist)
{
other.skiplist = nullptr;
}
~Accessor()
{
if (skiplist == nullptr) return;
skiplist->gc.release_ref();
}
Iterator begin() { return skiplist->begin(); }
ConstIterator begin() const { return skiplist->cbegin(); }
ConstIterator cbegin() const { return skiplist->cbegin(); }
Iterator end() { return skiplist->end(); }
ConstIterator end() const { return skiplist->cend(); }
ConstIterator cend() const { return skiplist->cend(); }
template <class K>
MultiIterator<K> end(const K &data)
{
return skiplist->mend(data);
}
template <class K>
MultiIterator<K> mend(const K &data)
{
return skiplist->template mend<K>(data);
}
std::pair<Iterator, bool> insert(const T &item)
{
return skiplist->insert(item, preds, succs);
}
std::pair<Iterator, bool> insert(T &&item)
{
return skiplist->insert(std::forward<T>(item), preds, succs);
}
Iterator insert_non_unique(const T &item)
{
return skiplist->insert_non_unique(item, preds, succs);
}
Iterator insert_non_unique(T &&item)
{
return skiplist->insert_non_unique(std::forward<T>(item), preds,
succs);
}
template <class K>
MultiIterator<K> find_multi(const K &item) const
{
return MultiIterator<K>(this->skiplist, item);
}
template <class K>
ConstIterator find(const K &item) const
{
return static_cast<const SkipList &>(*skiplist).find(item);
}
template <class K>
Iterator find(const K &item)
{
return skiplist->find(item);
}
template <class K>
bool contains(const K &item) const
{
return this->find(item) != this->end();
}
template <class K>
bool remove(const K &item)
{
return skiplist->remove(item, preds, succs,
SkipList<T>::template find_path<K>);
}
size_t size() const { return skiplist->size(); }
private:
SkipList *skiplist;
Node *preds[H], *succs[H];
};
Accessor access() { return Accessor(this); }
const Accessor access() const { return Accessor(this); }
private:
using guard_t = std::unique_lock<lock_t>;
Iterator begin() { return Iterator(header->forward(0)); }
ConstIterator begin() const { return ConstIterator(header->forward(0)); }
ConstIterator cbegin() const { return ConstIterator(header->forward(0)); }
Iterator end() { return Iterator(); }
ConstIterator end() const { return ConstIterator(); }
ConstIterator cend() const { return ConstIterator(); }
template <class K>
MultiIterator<K> end(const K &data)
{
return MultiIterator<K>(data);
}
template <class K>
MultiIterator<K> mend(const K &data)
{
return MultiIterator<K>(data);
}
size_t size() const { return count.load(); }
template <class K>
static bool greater(const K &item, const Node *const node)
{
return node && item > node->value();
}
template <class K>
static bool less(const K &item, const Node *const node)
{
return (node == nullptr) || item < node->value();
}
// Returns first occurence of item if there exists one.
template <class K>
ConstIterator find(const K &item) const
{
return const_cast<SkipList *>(this)->find_node<ConstIterator, K>(item);
}
// Returns first occurence of item if there exists one.
template <class K>
Iterator find(const K &item)
{
return find_node<Iterator, K>(item);
}
template <class It, class K>
It find_node(const K &item)
{
Node *node, *pred = header;
int h = static_cast<int>(pred->height) - 1;
while (true) {
// try to descend down first the next key on this layer overshoots
for (; h >= 0 && less(item, node = pred->forward(h)); --h) {
}
// if we overshoot at every layer, item doesn't exist
if (h < 0) return It();
// the item is farther to the right, continue going right as long
// as the key is greater than the current node's key
while (greater(item, node))
pred = node, node = node->forward(h);
// check if we have a hit. if not, we need to descend down again
if (!less(item, node) && !node->flags.is_marked()) return It(node);
}
}
template <class K>
static int find_path(SkipList *skiplist, int start, const K &item,
Node *preds[], Node *succs[])
{
int level_found = -1;
Node *pred = skiplist->header;
for (int level = start; level >= 0; --level) {
Node *node = pred->forward(level);
while (greater(item, node))
pred = node, node = pred->forward(level);
if (level_found == -1 && !less(item, node)) level_found = level;
preds[level] = pred;
succs[level] = node;
}
return level_found;
}
template <bool ADDING>
static bool lock_nodes(uint8_t height, guard_t guards[], Node *preds[],
Node *succs[])
{
Node *prepred, *pred, *succ = nullptr;
bool valid = true;
for (int level = 0; valid && level < height; ++level) {
pred = preds[level], succ = succs[level];
if (pred != prepred)
guards[level] = pred->acquire_unique(), prepred = pred;
valid = !pred->flags.is_marked() && pred->forward(level) == succ;
if (ADDING)
valid = valid && (succ == nullptr || !succ->flags.is_marked());
}
return valid;
}
Iterator insert_non_unique(T &&data, Node *preds[], Node *succs[])
{
while (true) {
// TODO: before here was data.first
auto level = find_path(this, H - 1, data, preds, succs);
auto height = 1;
if (level != -1) {
auto found = succs[level];
if (found->flags.is_marked()) continue;
if (!found->flags.is_fully_linked()) {
usleep(250);
continue;
}
// TODO Optimization for avoiding degrading of skiplist to list.
// Somehow this operation is errornus.
// if (found->height == 1) { // To avoid linearization new
// element
// // will be at least 2 height and
// will
// // be added in front.
// height = rnd();
// // if (height == 1) height = 2;
// } else {
// Only level 0 will be used so the rest is irrelevant.
preds[0] = found;
succs[0] = found->forward(0);
// This maybe isn't necessary
auto next = succs[0];
if (next != nullptr) {
if (next->flags.is_marked()) continue;
if (!next->flags.is_fully_linked()) {
usleep(250);
continue;
}
}
} else {
height = rnd();
// Optimization which doesn't add any extra locking.
if (height == 1)
height = 2; // Same key list will be skipped more often.
}
guard_t guards[H];
// try to acquire the locks for predecessors up to the height of
// the new node. release the locks and try again if someone else
// has the locks
if (!lock_nodes<true>(height, guards, preds, succs)) continue;
return insert_here(std::forward<T>(data), preds, succs, height,
guards);
}
}
// Insert unique data
std::pair<Iterator, bool> insert(T &&data, Node *preds[], Node *succs[])
{
while (true) {
// TODO: before here was data.first
auto level = find_path(this, H - 1, data, preds, succs);
if (level != -1) {
auto found = succs[level];
if (found->flags.is_marked()) continue;
while (!found->flags.is_fully_linked())
usleep(250);
return {Iterator{succs[level]}, false};
}
auto height = rnd();
guard_t guards[H];
// try to acquire the locks for predecessors up to the height of
// the new node. release the locks and try again if someone else
// has the locks
if (!lock_nodes<true>(height, guards, preds, succs)) continue;
return {insert_here(std::forward<T>(data), preds, succs, height,
guards),
true};
}
}
// Inserts data to specified locked location.
Iterator insert_here(T &&data, Node *preds[], Node *succs[], int height,
guard_t guards[])
{
// you have the locks, create a new node
auto new_node = Node::create(std::forward<T>(data), height);
// link the predecessors and successors, e.g.
//
// 4 HEAD ... P ------------------------> S ... NULL
// 3 HEAD ... ... P -----> NEW ---------> S ... NULL
// 2 HEAD ... ... P -----> NEW -----> S ... ... NULL
// 1 HEAD ... ... ... P -> NEW -> S ... ... ... NULL
for (uint8_t level = 0; level < height; ++level) {
new_node->forward(level, succs[level]);
preds[level]->forward(level, new_node);
}
new_node->flags.set_fully_linked();
count.fetch_add(1);
return Iterator{new_node};
}
static bool ok_delete(Node *node, int level)
{
return node->flags.is_fully_linked() && node->height - 1 == level &&
!node->flags.is_marked();
}
// Remove item found with fp with arguments skiplist,preds and succs.
// fp has to fill preds and succs which reflect location of item or return
// -1 as in not found otherwise returns level on which the item was first
// found.
template <class K>
bool remove(const K &item, Node *preds[], Node *succs[],
int (*fp)(SkipList *, int, const K &, Node *[], Node *[]))
{
Node *node = nullptr;
guard_t node_guard;
bool marked = false;
int height = 0;
while (true) {
auto level = fp(this, H - 1, item, preds, succs);
if (!marked && (level == -1 || !ok_delete(succs[level], level)))
return false;
if (!marked) {
node = succs[level];
height = node->height;
node_guard = node->acquire_unique();
if (node->flags.is_marked()) return false;
node->flags.set_marked();
marked = true;
}
guard_t guards[H];
if (!lock_nodes<false>(height, guards, preds, succs)) continue;
for (int level = height - 1; level >= 0; --level)
preds[level]->forward(level, node->forward(level));
// TODO: review and test
gc.collect(node);
count.fetch_sub(1);
return true;
}
}
// number of elements
std::atomic<size_t> count{0};
Node *header;
SkiplistGC<Node> gc;
};
template <class T, size_t H, class lock_t>
thread_local FastBinomial<H> SkipList<T, H, lock_t>::rnd;