359 lines
9.3 KiB
C++
359 lines
9.3 KiB
C++
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <folly/Portability.h>
|
|
#include <folly/synchronization/detail/Sleeper.h>
|
|
|
|
#include <glog/logging.h>
|
|
|
|
#include <atomic>
|
|
#include <thread>
|
|
|
|
/// Linked list class templates used in the hazard pointer library:
|
|
/// - linked_list: Sequential linked list that uses a pre-existing
|
|
/// members next() and set_next();.
|
|
/// - shared_head_tail_list: Thread-safe linked list that maintains
|
|
/// head and tail pointers. Supports push and pop_all.
|
|
/// - shared_head_only_list: Thread-safe linked lockable list that
|
|
/// maintains only a head pointer. Supports push and pop_all.
|
|
|
|
namespace folly {
|
|
namespace hazptr_detail {
|
|
|
|
/**
|
|
* linked_list
|
|
*
|
|
* Template parameter Node must support set_next
|
|
*
|
|
*/
|
|
template <typename Node>
|
|
class linked_list {
|
|
Node* head_;
|
|
Node* tail_;
|
|
|
|
public:
|
|
linked_list() noexcept : head_(nullptr), tail_(nullptr) {}
|
|
|
|
explicit linked_list(Node* head, Node* tail) noexcept
|
|
: head_(head), tail_(tail) {}
|
|
|
|
Node* head() const noexcept {
|
|
return head_;
|
|
}
|
|
|
|
Node* tail() const noexcept {
|
|
return tail_;
|
|
}
|
|
|
|
bool empty() const noexcept {
|
|
return head() == nullptr;
|
|
}
|
|
|
|
void push(Node* node) noexcept {
|
|
node->set_next(nullptr);
|
|
if (tail_) {
|
|
tail_->set_next(node);
|
|
} else {
|
|
head_ = node;
|
|
}
|
|
tail_ = node;
|
|
}
|
|
|
|
void splice(linked_list& l) {
|
|
if (head() == nullptr) {
|
|
head_ = l.head();
|
|
} else {
|
|
tail_->set_next(l.head());
|
|
}
|
|
tail_ = l.tail();
|
|
l.clear();
|
|
}
|
|
|
|
void clear() {
|
|
head_ = nullptr;
|
|
tail_ = nullptr;
|
|
}
|
|
|
|
}; // linked_list
|
|
|
|
/**
|
|
* shared_head_tail_list
|
|
*
|
|
* Maintains head and tail pointers. Supports push and pop all
|
|
* operations. Pop all operation is wait-free.
|
|
*/
|
|
template <typename Node, template <typename> class Atom = std::atomic>
|
|
class shared_head_tail_list {
|
|
Atom<Node*> head_;
|
|
Atom<Node*> tail_;
|
|
|
|
public:
|
|
shared_head_tail_list() noexcept : head_(nullptr), tail_(nullptr) {}
|
|
|
|
shared_head_tail_list(shared_head_tail_list&& o) noexcept {
|
|
head_.store(o.head(), std::memory_order_relaxed);
|
|
tail_.store(o.tail(), std::memory_order_relaxed);
|
|
o.head_.store(nullptr, std::memory_order_relaxed);
|
|
o.tail_.store(nullptr, std::memory_order_relaxed);
|
|
}
|
|
|
|
shared_head_tail_list& operator=(shared_head_tail_list&& o) noexcept {
|
|
head_.store(o.head(), std::memory_order_relaxed);
|
|
tail_.store(o.tail(), std::memory_order_relaxed);
|
|
o.head_.store(nullptr, std::memory_order_relaxed);
|
|
o.tail_.store(nullptr, std::memory_order_relaxed);
|
|
return *this;
|
|
}
|
|
|
|
~shared_head_tail_list() {
|
|
DCHECK(head() == nullptr);
|
|
DCHECK(tail() == nullptr);
|
|
}
|
|
|
|
void push(Node* node) noexcept {
|
|
bool done = false;
|
|
while (!done) {
|
|
if (tail()) {
|
|
done = push_in_non_empty_list(node);
|
|
} else {
|
|
done = push_in_empty_list(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
linked_list<Node> pop_all() noexcept {
|
|
auto h = exchange_head();
|
|
auto t = (h != nullptr) ? exchange_tail() : nullptr;
|
|
return linked_list<Node>(h, t);
|
|
}
|
|
|
|
bool empty() const noexcept {
|
|
return head() == nullptr;
|
|
}
|
|
|
|
private:
|
|
Node* head() const noexcept {
|
|
return head_.load(std::memory_order_acquire);
|
|
}
|
|
|
|
Node* tail() const noexcept {
|
|
return tail_.load(std::memory_order_acquire);
|
|
}
|
|
|
|
void set_head(Node* node) noexcept {
|
|
head_.store(node, std::memory_order_release);
|
|
}
|
|
|
|
bool cas_head(Node* expected, Node* node) noexcept {
|
|
return head_.compare_exchange_weak(
|
|
expected, node, std::memory_order_acq_rel, std::memory_order_relaxed);
|
|
}
|
|
|
|
bool cas_tail(Node* expected, Node* node) noexcept {
|
|
return tail_.compare_exchange_weak(
|
|
expected, node, std::memory_order_acq_rel, std::memory_order_relaxed);
|
|
}
|
|
|
|
Node* exchange_head() noexcept {
|
|
return head_.exchange(nullptr, std::memory_order_acq_rel);
|
|
}
|
|
|
|
Node* exchange_tail() noexcept {
|
|
return tail_.exchange(nullptr, std::memory_order_acq_rel);
|
|
}
|
|
|
|
bool push_in_non_empty_list(Node* node) noexcept {
|
|
auto h = head();
|
|
if (h) {
|
|
node->set_next(h); // Node must support set_next
|
|
if (cas_head(h, node)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool push_in_empty_list(Node* node) noexcept {
|
|
Node* t = nullptr;
|
|
node->set_next(nullptr); // Node must support set_next
|
|
if (cas_tail(t, node)) {
|
|
set_head(node);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}; // shared_head_tail_list
|
|
|
|
/**
|
|
* shared_head_only_list
|
|
*
|
|
* A shared singly linked list that maintains only a head pointer. It
|
|
* supports pop all and push list operations. Optionally the list may
|
|
* be locked for pop all operations. Pop all operations have locked
|
|
* and wait-free variants. Push operations are always lock-free.
|
|
*
|
|
* Not all combinations of operationsa are mutually operable. The
|
|
* following are valid combinations:
|
|
* - push(kMayBeLocked), pop_all(kAlsoLock), push_unlock
|
|
* - push(kMayNotBeLocked), pop_all(kDontLock)
|
|
*
|
|
* Locking is reentrant to prevent self deadlock.
|
|
*/
|
|
template <typename Node, template <typename> class Atom = std::atomic>
|
|
class shared_head_only_list {
|
|
Atom<uintptr_t> head_{0}; // lowest bit is a lock for pop all
|
|
Atom<std::thread::id> owner_{std::thread::id()};
|
|
int reentrance_{0};
|
|
|
|
static constexpr uintptr_t kLockBit = 1u;
|
|
static constexpr uintptr_t kUnlocked = 0u;
|
|
|
|
public:
|
|
static constexpr bool kAlsoLock = true;
|
|
static constexpr bool kDontLock = false;
|
|
static constexpr bool kMayBeLocked = true;
|
|
static constexpr bool kMayNotBeLocked = false;
|
|
|
|
public:
|
|
void push(linked_list<Node>& l, bool may_be_locked) noexcept {
|
|
if (l.empty()) {
|
|
return;
|
|
}
|
|
auto oldval = head();
|
|
while (true) {
|
|
auto newval = reinterpret_cast<uintptr_t>(l.head());
|
|
auto ptrval = oldval;
|
|
auto lockbit = oldval & kLockBit;
|
|
if (may_be_locked == kMayBeLocked) {
|
|
ptrval -= lockbit;
|
|
newval += lockbit;
|
|
} else {
|
|
DCHECK_EQ(lockbit, kUnlocked);
|
|
}
|
|
auto ptr = reinterpret_cast<Node*>(ptrval);
|
|
l.tail()->set_next(ptr); // Node must support set_next
|
|
if (cas_head(oldval, newval)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Node* pop_all(bool lock) noexcept {
|
|
return lock == kAlsoLock ? pop_all_lock() : pop_all_no_lock();
|
|
}
|
|
|
|
void push_unlock(linked_list<Node>& l) noexcept {
|
|
DCHECK_EQ(owner(), std::this_thread::get_id());
|
|
uintptr_t lockbit;
|
|
if (reentrance_ > 0) {
|
|
DCHECK_EQ(reentrance_, 1);
|
|
--reentrance_;
|
|
lockbit = kLockBit;
|
|
} else {
|
|
clear_owner();
|
|
lockbit = kUnlocked;
|
|
}
|
|
DCHECK_EQ(reentrance_, 0);
|
|
while (true) {
|
|
auto oldval = head();
|
|
DCHECK_EQ(oldval & kLockBit, kLockBit); // Should be already locked
|
|
auto ptrval = oldval - kLockBit;
|
|
auto ptr = reinterpret_cast<Node*>(ptrval);
|
|
auto t = l.tail();
|
|
if (t) {
|
|
t->set_next(ptr); // Node must support set_next
|
|
}
|
|
auto newval =
|
|
(t == nullptr) ? ptrval : reinterpret_cast<uintptr_t>(l.head());
|
|
newval += lockbit;
|
|
if (cas_head(oldval, newval)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool check_lock() const noexcept {
|
|
return (head() & kLockBit) == kLockBit;
|
|
}
|
|
|
|
bool empty() const noexcept {
|
|
return head() == 0u;
|
|
}
|
|
|
|
private:
|
|
uintptr_t head() const noexcept {
|
|
return head_.load(std::memory_order_acquire);
|
|
}
|
|
|
|
uintptr_t exchange_head() noexcept {
|
|
auto newval = reinterpret_cast<uintptr_t>(nullptr);
|
|
auto oldval = head_.exchange(newval, std::memory_order_acq_rel);
|
|
return oldval;
|
|
}
|
|
|
|
bool cas_head(uintptr_t& oldval, uintptr_t newval) noexcept {
|
|
return head_.compare_exchange_weak(
|
|
oldval, newval, std::memory_order_acq_rel, std::memory_order_acquire);
|
|
}
|
|
|
|
std::thread::id owner() {
|
|
return owner_.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
void set_owner() {
|
|
DCHECK(owner() == std::thread::id());
|
|
owner_.store(std::this_thread::get_id(), std::memory_order_relaxed);
|
|
}
|
|
|
|
void clear_owner() {
|
|
owner_.store(std::thread::id(), std::memory_order_relaxed);
|
|
}
|
|
|
|
Node* pop_all_no_lock() noexcept {
|
|
auto oldval = exchange_head();
|
|
DCHECK_EQ(oldval & kLockBit, kUnlocked);
|
|
return reinterpret_cast<Node*>(oldval);
|
|
}
|
|
|
|
Node* pop_all_lock() noexcept {
|
|
folly::detail::Sleeper s;
|
|
while (true) {
|
|
auto oldval = head();
|
|
auto lockbit = oldval & kLockBit;
|
|
std::thread::id tid = std::this_thread::get_id();
|
|
if (lockbit == kUnlocked || owner() == tid) {
|
|
auto newval = reinterpret_cast<uintptr_t>(nullptr) + kLockBit;
|
|
if (cas_head(oldval, newval)) {
|
|
DCHECK_EQ(reentrance_, 0);
|
|
if (lockbit == kUnlocked) {
|
|
set_owner();
|
|
} else {
|
|
++reentrance_;
|
|
}
|
|
auto ptrval = oldval - lockbit;
|
|
return reinterpret_cast<Node*>(ptrval);
|
|
}
|
|
}
|
|
s.sleep();
|
|
}
|
|
}
|
|
}; // shared_head_only_list
|
|
|
|
} // namespace hazptr_detail
|
|
} // namespace folly
|