memgraph/data_structures/skiplist/skiplist.hpp

650 lines
18 KiB
C++
Raw Normal View History

#pragma once
#include <algorithm>
#include <memory>
2015-12-08 04:50:07 +08:00
#include <cassert>
#include "threading/sync/lockable.hpp"
#include "threading/sync/spinlock.hpp"
#include "utils/random/fast_binomial.hpp"
2015-12-08 04:50:07 +08:00
#include "memory/lazy_gc.hpp"
2015-12-09 01:30:20 +08:00
/* @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<K, T> skiplist;
*
* {
* auto accessor = skiplist.access();
*
* // inserts <key1, value1> 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_unique(key1, value1);
*
* // nothing gets inserted because key1 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_unique(key1, value1);
*
* // returns an iterator to the element pair <key1, value1>
* auto it = accessor.find(key1);
*
* // returns an empty iterator. it == accessor.end()
* auto it = accessor.find(key2);
*
* // iterate over all key value pairs
* for(auto it = accessor.begin(); it != accessor.end(); ++it)
* cout << it->first << " " << it->second;
*
* // range based for loops also work
* for(auto& e : accessor)
* cout << e.first << " " << e.second;
*
* accessor.remove(key1); // returns true
* accessor.remove(key1); // 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 K Type to use as the key
* @tparam T Type to use as the value
* @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 K, class T, size_t H=32, class lock_t=SpinLock>
2015-12-08 04:50:07 +08:00
class SkipList : LazyGC<SkipList<K, T, H, lock_t>>, Lockable<lock_t>
{
2015-12-08 04:50:07 +08:00
public:
2015-12-09 01:30:20 +08:00
// 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;
2015-12-09 01:30:20 +08:00
using value_type = std::pair<const K, T>;
2015-12-08 04:50:07 +08:00
2015-12-09 01:30:20 +08:00
/* @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(std::memory_order_acquire) & MARKED;
}
void set_marked()
{
flags.fetch_or(MARKED, std::memory_order_release);
}
bool is_fully_linked() const
{
return flags.load(std::memory_order_acquire) & FULLY_LINKED;
}
void set_fully_linked()
{
flags.fetch_or(FULLY_LINKED, std::memory_order_release);
}
private:
std::atomic<uint8_t> flags {0};
};
class Node : Lockable<lock_t>
{
public:
2015-12-08 04:50:07 +08:00
friend class SkipList;
2015-12-09 01:30:20 +08:00
value_type data;
const uint8_t height;
Flags flags;
2015-12-08 04:50:07 +08:00
static Node* create(const K& key, const T& item, uint8_t height)
{
return create({key, item}, height);
}
static Node* create(const K& key, T&& item, uint8_t height)
{
2015-12-09 01:30:20 +08:00
return create({key, std::forward<T>(item)}, height);
}
2015-12-09 01:30:20 +08:00
static Node* create(value_type&& data, 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));
// we have raw memory and we need to construct an object
// of type Node on it
2015-12-09 01:30:20 +08:00
return new (node) Node(std::forward<value_type>(data), height);
}
static void destroy(Node* node)
{
node->~Node();
free(node);
}
2015-12-08 04:50:07 +08:00
Node* forward(size_t level) const
{
2015-12-08 04:50:07 +08:00
return tower[level].load(std::memory_order_acquire);
}
2015-12-08 04:50:07 +08:00
void forward(size_t level, Node* next)
{
tower[level].store(next, std::memory_order_release);
}
private:
2015-12-09 01:30:20 +08:00
Node(value_type&& data, uint8_t height)
: data(std::move(data)), 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()
{
for(auto i = 0; i < height; ++i)
tower[i].~atomic();
}
// 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>
2015-12-08 04:50:07 +08:00
{
protected:
IteratorBase(Node* node) : node(node) {}
2015-12-08 04:50:07 +08:00
Node* node {nullptr};
2015-12-08 04:50:07 +08:00
public:
IteratorBase() = default;
IteratorBase(const IteratorBase&) = default;
2015-12-08 04:50:07 +08:00
2015-12-09 01:30:20 +08:00
value_type& operator*()
2015-12-08 04:50:07 +08:00
{
assert(node != nullptr);
return node->data;
}
2015-12-09 01:30:20 +08:00
value_type* operator->()
2015-12-08 04:50:07 +08:00
{
assert(node != nullptr);
return &node->data;
}
2015-12-09 01:30:20 +08:00
operator value_type&()
2015-12-08 04:50:07 +08:00
{
assert(node != nullptr);
return node->data;
}
It& operator++()
2015-12-08 04:50:07 +08:00
{
assert(node != nullptr);
node = node->forward(0);
return this->derived();
2015-12-08 04:50:07 +08:00
}
It& operator++(int)
2015-12-08 04:50:07 +08:00
{
return operator++();
}
friend bool operator==(const It& a, const It& b)
2015-12-08 04:50:07 +08:00
{
return a.node == b.node;
}
friend bool operator!=(const It& a, const It& b)
2015-12-08 04:50:07 +08:00
{
return !(a == b);
}
};
class ConstIterator : public IteratorBase<ConstIterator>
{
2015-12-08 04:50:07 +08:00
friend class SkipList;
ConstIterator(Node* node) : IteratorBase<ConstIterator>(node) {}
2015-12-08 04:50:07 +08:00
public:
ConstIterator() = default;
ConstIterator(const ConstIterator&) = default;
2015-12-09 01:30:20 +08:00
const value_type& operator*()
{
return IteratorBase<ConstIterator>::operator*();
}
2015-12-09 01:30:20 +08:00
const value_type* operator->()
2015-12-08 04:50:07 +08:00
{
return IteratorBase<ConstIterator>::operator->();
2015-12-08 04:50:07 +08:00
}
2015-12-09 01:30:20 +08:00
operator const value_type&()
2015-12-08 04:50:07 +08:00
{
2015-12-09 01:30:20 +08:00
return IteratorBase<ConstIterator>::operator value_type&();
2015-12-08 04:50:07 +08:00
}
};
2015-12-08 04:50:07 +08:00
class Iterator : public IteratorBase<Iterator>
{
friend class SkipList;
Iterator(Node* node) : IteratorBase<Iterator>(node) {}
public:
Iterator() = default;
Iterator(const Iterator&) = default;
};
SkipList() : header(Node::create(K(), std::move(T()), H)) {}
2015-12-08 04:50:07 +08:00
friend class Accessor;
class Accessor
{
friend class SkipList;
Accessor(SkipList* skiplist) : skiplist(skiplist)
{
assert(skiplist != nullptr);
// addref
}
2015-12-08 04:50:07 +08:00
public:
Accessor(const Accessor&) = delete;
Accessor(Accessor&& other) : skiplist(other.skiplist)
{
other.skiplist = nullptr;
}
~Accessor()
{
if(skiplist == nullptr)
return;
// releaseref
}
2015-12-08 04:50:07 +08:00
Iterator begin()
{
return skiplist->begin();
2015-12-08 04:50:07 +08:00
}
ConstIterator begin() const
{
return skiplist->cbegin();
2015-12-08 04:50:07 +08:00
}
ConstIterator cbegin() const
{
return skiplist->cbegin();
2015-12-08 04:50:07 +08:00
}
Iterator end()
{
return skiplist->end();
2015-12-08 04:50:07 +08:00
}
ConstIterator end() const
{
return skiplist->cend();
2015-12-08 04:50:07 +08:00
}
ConstIterator cend() const
{
return skiplist->cend();
2015-12-08 04:50:07 +08:00
}
std::pair<Iterator, bool> insert_unique(const K& key, const T& item)
{
return skiplist->insert({key, item}, preds, succs);
2015-12-08 04:50:07 +08:00
}
std::pair<Iterator, bool> insert_unique(const K& key, T&& item)
{
return skiplist->insert({key, std::forward<T>(item)}, preds, succs);
2015-12-08 04:50:07 +08:00
}
ConstIterator find(const K& key) const
{
return static_cast<const SkipList&>(*skiplist).find(key);
2015-12-08 04:50:07 +08:00
}
Iterator find(const K& key)
{
return skiplist->find(key);
2015-12-08 04:50:07 +08:00
}
bool remove(const K& key)
{
return skiplist->remove(key, preds, succs);
2015-12-08 04:50:07 +08:00
}
private:
SkipList* skiplist;
2015-12-08 04:50:07 +08:00
Node* preds[H], *succs[H];
};
Accessor access()
{
return Accessor(this);
2015-12-08 04:50:07 +08:00
}
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();
}
size_t size() const
{
return count.load(std::memory_order_acquire);
}
2015-12-08 04:50:07 +08:00
bool greater(const K& key, const Node* const node)
{
2015-12-08 04:50:07 +08:00
return node && key > node->data.first;
}
2015-12-08 04:50:07 +08:00
bool less(const K& key, const Node* const node)
{
return (node == nullptr) || key < node->data.first;
}
ConstIterator find(const K& key) const
{
return const_cast<SkipList*>(this)->find_node<ConstIterator>(key);
2015-12-08 04:50:07 +08:00
}
Iterator find(const K& key)
{
return find_node<Iterator>(key);
}
template <class It>
It find_node(const K& key)
{
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(key, node = pred->forward(h)); --h) {}
// if we overshoot at every layer, item doesn't exist
if(h < 0)
2015-12-08 04:50:07 +08:00
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(key, node))
pred = node, node = node->forward(h);
// check if we have a hit. if not, we need to descend down again
2015-12-08 04:50:07 +08:00
if(!less(key, node) && !node->flags.is_marked())
return It(node);
}
}
2015-12-08 04:50:07 +08:00
int find_path(Node* from, int start, const K& key,
Node* preds[], Node* succs[])
{
int level_found = -1;
Node* pred = from;
2015-12-08 04:50:07 +08:00
for(int level = start; level >= 0; --level)
{
Node* node = pred->forward(level);
while(greater(key, node))
pred = node, node = pred->forward(level);
if(level_found == -1 && !less(key, node))
level_found = level;
preds[level] = pred;
succs[level] = node;
}
return level_found;
}
template <bool ADDING>
bool lock_nodes(uint8_t height, guard_t guards[],
Node* preds[], Node* succs[])
{
2015-12-08 04:50:07 +08:00
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;
}
2015-12-08 04:50:07 +08:00
std::pair<Iterator, bool>
2015-12-09 01:30:20 +08:00
insert(value_type&& data, Node* preds[], Node* succs[])
{
2015-12-08 04:50:07 +08:00
while(true)
{
auto level = find_path(header, H - 1, data.first, 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;
// you have the locks, create a new node
2015-12-09 01:30:20 +08:00
auto new_node = Node::create(std::forward<value_type>(data), height);
2015-12-08 04:50:07 +08:00
// 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, std::memory_order_relaxed);
return {Iterator {new_node}, true};
}
}
2015-12-08 04:50:07 +08:00
bool ok_delete(Node* node, int level)
{
2015-12-08 04:50:07 +08:00
return node->flags.is_fully_linked()
&& node->height - 1 == level
&& !node->flags.is_marked();
}
2015-12-08 04:50:07 +08:00
bool remove(const K& key, Node* preds[], Node* succs[])
{
Node* node = nullptr;
guard_t node_guard;
bool marked = false;
int height = 0;
while(true)
{
2015-12-08 04:50:07 +08:00
auto level = find_path(header, H - 1, key, preds, succs);
2015-12-08 04:50:07 +08:00
if(!marked && (level == -1 || !ok_delete(succs[level], level)))
return false;
2015-12-08 04:50:07 +08:00
if(!marked)
{
node = succs[level];
height = node->height;
node_guard = node->acquire_unique();
2015-12-08 04:50:07 +08:00
if(node->flags.is_marked())
return false;
2015-12-08 04:50:07 +08:00
node->flags.set_marked();
}
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 recycle(node)
count.fetch_sub(1, std::memory_order_relaxed);
return true;
}
2015-12-08 04:50:07 +08:00
}
guard_t gc_lock_acquire()
{
return this->acquire_unique();
}
void vacuum()
{
}
2015-12-08 04:50:07 +08:00
std::atomic<size_t> count {0};
Node* header;
};
2015-12-08 04:50:07 +08:00
template <class K, class T, size_t H, class lock_t>
thread_local FastBinomial<H> SkipList<K, T, H, lock_t>::rnd;