Merge branch 'master' of https://phabricator.tomicevic.com/diffusion/MG/memgraph
This commit is contained in:
commit
36856f3bfb
@ -2,22 +2,26 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <cassert>
|
||||
|
||||
#include "threading/hazard_ptr.hpp"
|
||||
#include "threading/sync/lockable.hpp"
|
||||
#include "threading/sync/spinlock.hpp"
|
||||
|
||||
#include "utils/random/fast_binomial.hpp"
|
||||
#include "memory/lazy_gc.hpp"
|
||||
|
||||
// concurrent skiplist based on the implementation described in
|
||||
// "A Provably Correct Scalable Concurrent Skip List"
|
||||
// https://www.cs.tau.ac.il/~shanir/nir-pubs-web/Papers/OPODIS2006-BA.pdf
|
||||
|
||||
template <class K, class T, size_t H=32, class lock_t=SpinLock>
|
||||
class SkipList
|
||||
class SkipList : LazyGC<SkipList<K, T, H, lock_t>>, Lockable<lock_t>
|
||||
{
|
||||
public:
|
||||
static thread_local FastBinomial<H> rnd;
|
||||
|
||||
using data_t = std::pair<const K, T>;
|
||||
|
||||
struct Flags
|
||||
{
|
||||
enum node_flags : uint8_t {
|
||||
@ -51,62 +55,20 @@ class SkipList
|
||||
|
||||
class Node : Lockable<lock_t>
|
||||
{
|
||||
public:
|
||||
friend class SkipList;
|
||||
|
||||
public:
|
||||
class reference
|
||||
{
|
||||
public:
|
||||
reference() {}
|
||||
|
||||
reference(Node* node, bool store_hazard = true)
|
||||
: node(node), hazard(store_hazard ? node : nullptr) {}
|
||||
|
||||
reference(const reference&) = delete;
|
||||
|
||||
reference(reference&& other)
|
||||
: node(other.node), hazard(std::move(other.hazard))
|
||||
{
|
||||
other.node = nullptr;
|
||||
}
|
||||
|
||||
reference& operator=(const reference& other) = delete;
|
||||
|
||||
reference& operator=(const reference&& other)
|
||||
{
|
||||
node = other.node;
|
||||
other.node = nullptr;
|
||||
hazard = std::move(other.hazard);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
T& operator*() { return *node; }
|
||||
const T& operator*() const { return *node; }
|
||||
|
||||
T* operator->() { return node; }
|
||||
const T* operator->() const { return node; }
|
||||
|
||||
operator T*() { return node; }
|
||||
const operator T*() const { return node; }
|
||||
|
||||
const hazard_ptr& get_hazard() const
|
||||
{
|
||||
return hazard;
|
||||
}
|
||||
|
||||
private:
|
||||
hazard_ptr hazard;
|
||||
Node* node;
|
||||
};
|
||||
|
||||
K key;
|
||||
T item;
|
||||
data_t data;
|
||||
|
||||
const uint8_t height;
|
||||
Flags flags;
|
||||
|
||||
static Node* create(K key, T item, uint8_t height)
|
||||
static Node* create(const K& key, const T& item, uint8_t height)
|
||||
{
|
||||
return create({key, item}, height);
|
||||
}
|
||||
|
||||
static Node* create(data_t&& data, uint8_t height)
|
||||
{
|
||||
// [ Node ][Node*][Node*][Node*]...[Node*]
|
||||
// | | | | |
|
||||
@ -120,7 +82,7 @@ class SkipList
|
||||
|
||||
// we have raw memory and we need to construct an object
|
||||
// of type Node on it
|
||||
return new (node) Node(key, item, height);
|
||||
return new (node) Node(std::move(data), height);
|
||||
}
|
||||
|
||||
static void destroy(Node* node)
|
||||
@ -129,20 +91,19 @@ class SkipList
|
||||
free(node);
|
||||
}
|
||||
|
||||
Node::reference forward(size_t level) const
|
||||
Node* forward(size_t level) const
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
auto ref = Node::reference(load_tower(level));
|
||||
return tower[level].load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
if(ref.get_hazard() == load_tower(level))
|
||||
return std::move(ref);
|
||||
}
|
||||
void forward(size_t level, Node* next)
|
||||
{
|
||||
tower[level].store(next, std::memory_order_release);
|
||||
}
|
||||
|
||||
private:
|
||||
Node(K key, T item, uint8_t height)
|
||||
: key(key), item(item), height(height)
|
||||
Node(data_t data, uint8_t height)
|
||||
: data(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
|
||||
@ -157,16 +118,6 @@ class SkipList
|
||||
tower[i].~atomic();
|
||||
}
|
||||
|
||||
void forward(size_t level, Node* next)
|
||||
{
|
||||
tower[level].store(next, std::memory_order_release);
|
||||
}
|
||||
|
||||
Node* load_tower(size_t level)
|
||||
{
|
||||
return tower[level].load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -176,52 +127,254 @@ class SkipList
|
||||
std::atomic<Node*> tower[0];
|
||||
};
|
||||
|
||||
using node_ref_t = typename Node::reference;
|
||||
|
||||
public:
|
||||
class iterator
|
||||
class ConstIterator
|
||||
{
|
||||
friend class SkipList;
|
||||
ConstIterator(Node* node) : node(node) {}
|
||||
|
||||
public:
|
||||
iterator() = default;
|
||||
iterator(node_ref_t&& node) : node(std::move(node)) {}
|
||||
iterator(const iterator&) = delete;
|
||||
ConstIterator() = default;
|
||||
ConstIterator(const ConstIterator&) = default;
|
||||
|
||||
T& operator*() { return node; }
|
||||
T* operator->() { return node; }
|
||||
|
||||
operator T*() { return node; }
|
||||
|
||||
iterator& operator++()
|
||||
const data_t& operator*()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
return node->data;
|
||||
}
|
||||
|
||||
const data_t* operator->()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
return &node->data;
|
||||
}
|
||||
|
||||
operator const data_t()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
return node->data;
|
||||
}
|
||||
|
||||
ConstIterator& operator++()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
node = node->forward(0);
|
||||
return *this;
|
||||
}
|
||||
|
||||
iterator& operator++(int)
|
||||
ConstIterator& operator++(int)
|
||||
{
|
||||
return operator++();
|
||||
}
|
||||
|
||||
friend bool operator==(const ConstIterator& a, const ConstIterator& b)
|
||||
{
|
||||
return a.node == b.node;
|
||||
}
|
||||
|
||||
friend bool operator!=(const ConstIterator& a, const ConstIterator& b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
private:
|
||||
node_ref_t node;
|
||||
Node* node {nullptr};
|
||||
};
|
||||
|
||||
class Iterator
|
||||
{
|
||||
friend class SkipList;
|
||||
Iterator(Node* node) : node(node) {}
|
||||
|
||||
public:
|
||||
Iterator() = default;
|
||||
Iterator(const Iterator&) = default;
|
||||
|
||||
data_t& operator*()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
return node->data;
|
||||
}
|
||||
|
||||
data_t* operator->()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
return &node->data;
|
||||
}
|
||||
|
||||
operator data_t()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
return node->data;
|
||||
}
|
||||
|
||||
Iterator& operator++()
|
||||
{
|
||||
assert(node != nullptr);
|
||||
node = node->forward(0);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Iterator& operator++(int)
|
||||
{
|
||||
return operator++();
|
||||
}
|
||||
|
||||
friend bool operator==(const Iterator& a, const Iterator& b)
|
||||
{
|
||||
return a.node == b.node;
|
||||
}
|
||||
|
||||
friend bool operator!=(const Iterator& a, const Iterator& b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
private:
|
||||
Node* node {nullptr};
|
||||
};
|
||||
|
||||
SkipList() : header(Node::create(K(), T(), H)) {}
|
||||
|
||||
auto begin() { return iterator(header->forward(0)); }
|
||||
auto end() { return iterator(); }
|
||||
friend class Accessor;
|
||||
|
||||
class Accessor
|
||||
{
|
||||
friend class SkipList;
|
||||
|
||||
Accessor(SkipList& skiplist) : skiplist(skiplist) {}
|
||||
public:
|
||||
Accessor(const Accessor&) = delete;
|
||||
Accessor(Accessor&&) = default;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
std::pair<Iterator, bool> insert_unique(const K& key, const T& item)
|
||||
{
|
||||
return skiplist.insert({key, item}, preds, succs);
|
||||
}
|
||||
|
||||
std::pair<Iterator, bool> insert_unique(const K& key, T&& item)
|
||||
{
|
||||
return skiplist.insert({key, std::move(item)}, preds, succs);
|
||||
}
|
||||
|
||||
ConstIterator find(const K& key) const
|
||||
{
|
||||
return skiplist.find(key);
|
||||
}
|
||||
|
||||
Iterator find(const K& key)
|
||||
{
|
||||
return skiplist.find(key);
|
||||
}
|
||||
|
||||
bool remove(const K& key)
|
||||
{
|
||||
return skiplist.remove(key, preds, succs);
|
||||
}
|
||||
|
||||
private:
|
||||
SkipList& skiplist;
|
||||
Node* preds[H], *succs[H];
|
||||
};
|
||||
|
||||
Accessor access()
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return count.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
iterator find(const K& key)
|
||||
bool greater(const K& key, const Node* const node)
|
||||
{
|
||||
auto pred = node_ref_t(header);
|
||||
node_ref_t node;
|
||||
return node && key > node->data.first;
|
||||
}
|
||||
|
||||
uint8_t h = pred->height - 1;
|
||||
bool less(const K& key, const Node* const node)
|
||||
{
|
||||
return (node == nullptr) || key < node->data.first;
|
||||
}
|
||||
|
||||
ConstIterator find(const K& key) const
|
||||
{
|
||||
return find_node<ConstIterator>(key);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@ -230,7 +383,7 @@ public:
|
||||
|
||||
// if we overshoot at every layer, item doesn't exist
|
||||
if(h < 0)
|
||||
return iterator();
|
||||
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
|
||||
@ -238,47 +391,169 @@ public:
|
||||
pred = node, node = node->forward(h);
|
||||
|
||||
// check if we have a hit. if not, we need to descend down again
|
||||
if(!less(key, node))
|
||||
return iterator(std::move(node));
|
||||
if(!less(key, node) && !node->flags.is_marked())
|
||||
return It(node);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<size_t> count {0};
|
||||
Node* header;
|
||||
|
||||
bool greater(const K& key, const Node* const node)
|
||||
int find_path(Node* from, int start, const K& key,
|
||||
Node* preds[], Node* succs[])
|
||||
{
|
||||
return node && key > node->key;
|
||||
}
|
||||
|
||||
bool less(const K& key, const Node* const node)
|
||||
{
|
||||
return (node == nullptr) || key < node->key;
|
||||
}
|
||||
|
||||
int find_path(Node& from, int start, const K& key,
|
||||
node_ref_t (preds&)[],
|
||||
node_ref_t (succs&)[])
|
||||
{
|
||||
int lfound = -1;
|
||||
int level_found = -1;
|
||||
Node* pred = from;
|
||||
|
||||
for(int level = start_level; level >= 0; --level)
|
||||
for(int level = start; level >= 0; --level)
|
||||
{
|
||||
Node* node = pred->forward(level);
|
||||
|
||||
while(greater(key, node))
|
||||
pred = node, node = pred->forward(level);
|
||||
|
||||
|
||||
if(lfound == -1 && !less(key, node))
|
||||
lfound = level;
|
||||
if(level_found == -1 && !less(key, node))
|
||||
level_found = level;
|
||||
|
||||
preds[level] = pred;
|
||||
succs[level] = node;
|
||||
}
|
||||
|
||||
return lfound;
|
||||
return level_found;
|
||||
}
|
||||
|
||||
template <bool ADDING>
|
||||
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;
|
||||
}
|
||||
|
||||
std::pair<Iterator, bool>
|
||||
insert(data_t&& data, Node* preds[], Node* succs[])
|
||||
{
|
||||
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
|
||||
auto new_node = Node::create(std::move(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, std::memory_order_relaxed);
|
||||
|
||||
return {Iterator {new_node}, true};
|
||||
}
|
||||
}
|
||||
|
||||
bool ok_delete(Node* node, int level)
|
||||
{
|
||||
return node->flags.is_fully_linked()
|
||||
&& node->height - 1 == level
|
||||
&& !node->flags.is_marked();
|
||||
}
|
||||
|
||||
bool remove(const K& key, Node* preds[], Node* succs[])
|
||||
{
|
||||
Node* node = nullptr;
|
||||
guard_t node_guard;
|
||||
bool marked = false;
|
||||
int height = 0;
|
||||
|
||||
while(true)
|
||||
{
|
||||
auto level = find_path(header, H - 1, key, 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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
guard_t gc_lock_acquire()
|
||||
{
|
||||
return this->acquire_unique();
|
||||
}
|
||||
|
||||
void vacuum()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
std::atomic<size_t> count {0};
|
||||
Node* header;
|
||||
};
|
||||
|
||||
template <class K, class T, size_t H, class lock_t>
|
||||
thread_local FastBinomial<H> SkipList<K, T, H, lock_t>::rnd;
|
||||
|
||||
|
Binary file not shown.
@ -2,9 +2,52 @@
|
||||
|
||||
#include "data_structures/skiplist/skiplist.hpp"
|
||||
|
||||
using std::cout;
|
||||
using std::endl;
|
||||
|
||||
using skiplist_t = SkipList<int, int>;
|
||||
|
||||
void print_skiplist(const skiplist_t::Accessor& skiplist)
|
||||
{
|
||||
cout << "---- skiplist now has: ";
|
||||
|
||||
for(auto& kv : skiplist)
|
||||
cout << "(" << kv.first << ", " << kv.second << ") ";
|
||||
|
||||
cout << "----" << endl;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
auto skiplist = new SkipList<int, int>();
|
||||
cout << std::boolalpha;
|
||||
skiplist_t skiplist;
|
||||
|
||||
auto accessor = skiplist.access();
|
||||
|
||||
cout << "added non-existing (1, 10)? (true) "
|
||||
<< accessor.insert_unique(1, 10).second << endl;
|
||||
|
||||
cout << "added already existing (1, 10)? (false) "
|
||||
<< accessor.insert_unique(1, 10).second << endl;
|
||||
|
||||
accessor.insert_unique(2, 20);
|
||||
print_skiplist(accessor);
|
||||
|
||||
cout << "value at key 3 exists? (false) "
|
||||
<< (accessor.find(3) == accessor.end()) << endl;
|
||||
|
||||
cout << "value at key 2 exists? (true) "
|
||||
<< (accessor.find(2) != accessor.end()) << endl;
|
||||
|
||||
cout << "at key 2 is? (20) " << accessor.find(2)->second << endl;
|
||||
|
||||
cout << "removed existing (1)? (true) " << accessor.remove(1) << endl;
|
||||
cout << "removed non-existing (3)? (false) " << accessor.remove(3) << endl;
|
||||
|
||||
accessor.insert_unique(1, 10);
|
||||
accessor.insert_unique(4, 40);
|
||||
|
||||
print_skiplist(accessor);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
29
memory/deferred_recycler.hpp
Normal file
29
memory/deferred_recycler.hpp
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "recycler.hpp"
|
||||
|
||||
template <class T, class Allocator>
|
||||
class DeferredRecycler : Recycler<T, Allocator>
|
||||
{
|
||||
public:
|
||||
using Recycler<T, Allocator>::acquire;
|
||||
|
||||
void recycle(T* item)
|
||||
{
|
||||
auto guard = this->acquire_unique();
|
||||
dirty.push_back(item);
|
||||
}
|
||||
|
||||
void clean()
|
||||
{
|
||||
auto guard = this->acquire_unique();
|
||||
|
||||
for(auto item : dirty)
|
||||
this->recycle_or_delete(item);
|
||||
|
||||
dirty.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::queue<T*> dirty;
|
||||
};
|
@ -33,6 +33,8 @@ public:
|
||||
return;
|
||||
|
||||
this->derived().vacuum();
|
||||
|
||||
dirty.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
@ -7,7 +7,7 @@
|
||||
#include "threading/sync/spinlock.hpp"
|
||||
|
||||
template <class T, class Allocator=std::allocator<T>>
|
||||
class Recycler : Lockable<SpinLock>
|
||||
class Recycler : public Lockable<SpinLock>
|
||||
{
|
||||
static constexpr size_t default_max_reserved = 100;
|
||||
|
||||
@ -19,17 +19,28 @@ public:
|
||||
T* acquire(Args&&... args)
|
||||
{
|
||||
auto guard = acquire_unique();
|
||||
return new T(std::forward<Args>(args)...); // todo refactor :D
|
||||
return fetch_or_create(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
void release(T* item)
|
||||
{
|
||||
auto guard = acquire_unique();
|
||||
delete item; // todo refactor :D
|
||||
return recycle_or_delete(item);
|
||||
}
|
||||
|
||||
private:
|
||||
protected:
|
||||
Allocator alloc;
|
||||
size_t max_reserved {default_max_reserved};
|
||||
std::queue<T*> items;
|
||||
|
||||
template <class... Args>
|
||||
T* fetch_or_create(Args&&... args)
|
||||
{
|
||||
return new T(std::forward<Args>(args)...); // todo refactor :D
|
||||
}
|
||||
|
||||
void recycle_or_delete(T* item)
|
||||
{
|
||||
delete item; // todo refactor :D
|
||||
}
|
||||
};
|
||||
|
@ -17,7 +17,7 @@
|
||||
* and the app is left in an inconsistent state. ideally, you would like
|
||||
* to call resource.disable regardles of the exception being thrown.
|
||||
* OnScopeExit makes this possible and very convenient via a 'Auto' macro
|
||||
*
|
||||
*
|
||||
* void hard_worker()
|
||||
* {
|
||||
* resource.enable();
|
||||
|
Loading…
Reference in New Issue
Block a user