251 lines
7.6 KiB
C++
251 lines
7.6 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.
|
|
*/
|
|
|
|
#include <folly/CancellationToken.h>
|
|
#include <folly/Optional.h>
|
|
#include <folly/synchronization/detail/Sleeper.h>
|
|
|
|
#include <glog/logging.h>
|
|
|
|
#include <algorithm>
|
|
#include <new>
|
|
#include <thread>
|
|
#include <tuple>
|
|
|
|
namespace folly {
|
|
namespace detail {
|
|
|
|
CancellationState::~CancellationState() {
|
|
DCHECK(head_ == nullptr);
|
|
DCHECK(!isLocked(state_.load(std::memory_order_relaxed)));
|
|
DCHECK(
|
|
state_.load(std::memory_order_relaxed) < kTokenReferenceCountIncrement);
|
|
}
|
|
|
|
bool CancellationState::tryAddCallback(
|
|
CancellationCallback* callback,
|
|
bool incrementRefCountIfSuccessful) noexcept {
|
|
// Try to acquire the lock, but abandon trying to acquire the lock if
|
|
// cancellation has already been requested (we can just immediately invoke
|
|
// the callback) or if cancellation can never be requested (we can just
|
|
// skip registration).
|
|
if (!tryLock([callback](std::uint64_t oldState) noexcept {
|
|
if (isCancellationRequested(oldState)) {
|
|
callback->invokeCallback();
|
|
return false;
|
|
}
|
|
return canBeCancelled(oldState);
|
|
})) {
|
|
return false;
|
|
}
|
|
|
|
// We've acquired the lock and cancellation has not yet been requested.
|
|
// Push this callback onto the head of the list.
|
|
if (head_ != nullptr) {
|
|
head_->prevNext_ = &callback->next_;
|
|
}
|
|
callback->next_ = head_;
|
|
callback->prevNext_ = &head_;
|
|
head_ = callback;
|
|
|
|
if (incrementRefCountIfSuccessful) {
|
|
// Combine multiple atomic operations into a single atomic operation.
|
|
unlockAndIncrementTokenCount();
|
|
} else {
|
|
unlock();
|
|
}
|
|
|
|
// Successfully added the callback.
|
|
return true;
|
|
}
|
|
|
|
void CancellationState::removeCallback(
|
|
CancellationCallback* callback) noexcept {
|
|
DCHECK(callback != nullptr);
|
|
|
|
lock();
|
|
|
|
if (callback->prevNext_ != nullptr) {
|
|
// Still registered in the list => not yet executed.
|
|
// Just remove it from the list.
|
|
*callback->prevNext_ = callback->next_;
|
|
if (callback->next_ != nullptr) {
|
|
callback->next_->prevNext_ = callback->prevNext_;
|
|
}
|
|
|
|
unlockAndDecrementTokenCount();
|
|
return;
|
|
}
|
|
|
|
unlock();
|
|
|
|
// Callback has either already executed or is executing concurrently on
|
|
// another thread.
|
|
|
|
if (signallingThreadId_ == std::this_thread::get_id()) {
|
|
// Callback executed on this thread or is still currently executing
|
|
// and is deregistering itself from within the callback.
|
|
if (callback->destructorHasRunInsideCallback_ != nullptr) {
|
|
// Currently inside the callback, let the requestCancellation() method
|
|
// know the object is about to be destructed and that it should
|
|
// not try to access the object when the callback returns.
|
|
*callback->destructorHasRunInsideCallback_ = true;
|
|
}
|
|
} else {
|
|
// Callback is currently executing on another thread, block until it
|
|
// finishes executing.
|
|
folly::detail::Sleeper sleeper;
|
|
while (!callback->callbackCompleted_.load(std::memory_order_acquire)) {
|
|
sleeper.wait();
|
|
}
|
|
}
|
|
|
|
removeTokenReference();
|
|
}
|
|
|
|
bool CancellationState::requestCancellation() noexcept {
|
|
if (!tryLockAndCancelUnlessCancelled()) {
|
|
// Was already marked as cancelled
|
|
return true;
|
|
}
|
|
|
|
// This thread marked as cancelled and acquired the lock
|
|
|
|
signallingThreadId_ = std::this_thread::get_id();
|
|
|
|
while (head_ != nullptr) {
|
|
// Dequeue the first item on the queue.
|
|
CancellationCallback* callback = head_;
|
|
head_ = callback->next_;
|
|
const bool anyMore = head_ != nullptr;
|
|
if (anyMore) {
|
|
head_->prevNext_ = &head_;
|
|
}
|
|
// Mark this item as removed from the list.
|
|
callback->prevNext_ = nullptr;
|
|
|
|
// Don't hold the lock while executing the callback
|
|
// as we don't want to block other threads from
|
|
// deregistering callbacks.
|
|
unlock();
|
|
|
|
// TRICKY: Need to store a flag on the stack here that the callback
|
|
// can use to signal that the destructor was executed inline
|
|
// during the call.
|
|
// If the destructor was executed inline then it's not safe to
|
|
// dereference 'callback' after 'invokeCallback()' returns.
|
|
// If the destructor runs on some other thread then the other
|
|
// thread will block waiting for this thread to signal that the
|
|
// callback has finished executing.
|
|
bool destructorHasRunInsideCallback = false;
|
|
callback->destructorHasRunInsideCallback_ = &destructorHasRunInsideCallback;
|
|
|
|
callback->invokeCallback();
|
|
|
|
if (!destructorHasRunInsideCallback) {
|
|
callback->destructorHasRunInsideCallback_ = nullptr;
|
|
callback->callbackCompleted_.store(true, std::memory_order_release);
|
|
}
|
|
|
|
if (!anyMore) {
|
|
// This was the last item in the queue when we dequeued it.
|
|
// No more items should be added to the queue after we have
|
|
// marked the state as cancelled, only removed from the queue.
|
|
// Avoid acquring/releasing the lock in this case.
|
|
return false;
|
|
}
|
|
|
|
lock();
|
|
}
|
|
|
|
unlock();
|
|
|
|
return false;
|
|
}
|
|
|
|
void CancellationState::lock() noexcept {
|
|
folly::detail::Sleeper sleeper;
|
|
std::uint64_t oldState = state_.load(std::memory_order_relaxed);
|
|
do {
|
|
while (isLocked(oldState)) {
|
|
sleeper.wait();
|
|
oldState = state_.load(std::memory_order_relaxed);
|
|
}
|
|
} while (!state_.compare_exchange_weak(
|
|
oldState,
|
|
oldState | kLockedFlag,
|
|
std::memory_order_acquire,
|
|
std::memory_order_relaxed));
|
|
}
|
|
|
|
void CancellationState::unlock() noexcept {
|
|
state_.fetch_sub(kLockedFlag, std::memory_order_release);
|
|
}
|
|
|
|
void CancellationState::unlockAndIncrementTokenCount() noexcept {
|
|
state_.fetch_sub(
|
|
kLockedFlag - kTokenReferenceCountIncrement, std::memory_order_release);
|
|
}
|
|
|
|
void CancellationState::unlockAndDecrementTokenCount() noexcept {
|
|
auto oldState = state_.fetch_sub(
|
|
kLockedFlag + kTokenReferenceCountIncrement, std::memory_order_acq_rel);
|
|
if (oldState < (kLockedFlag + 2 * kTokenReferenceCountIncrement)) {
|
|
delete this;
|
|
}
|
|
}
|
|
|
|
bool CancellationState::tryLockAndCancelUnlessCancelled() noexcept {
|
|
folly::detail::Sleeper sleeper;
|
|
std::uint64_t oldState = state_.load(std::memory_order_acquire);
|
|
while (true) {
|
|
if (isCancellationRequested(oldState)) {
|
|
return false;
|
|
} else if (isLocked(oldState)) {
|
|
sleeper.wait();
|
|
oldState = state_.load(std::memory_order_acquire);
|
|
} else if (state_.compare_exchange_weak(
|
|
oldState,
|
|
oldState | kLockedFlag | kCancellationRequestedFlag,
|
|
std::memory_order_acq_rel,
|
|
std::memory_order_acquire)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename Predicate>
|
|
bool CancellationState::tryLock(Predicate predicate) noexcept {
|
|
folly::detail::Sleeper sleeper;
|
|
std::uint64_t oldState = state_.load(std::memory_order_acquire);
|
|
while (true) {
|
|
if (!predicate(oldState)) {
|
|
return false;
|
|
} else if (isLocked(oldState)) {
|
|
sleeper.wait();
|
|
oldState = state_.load(std::memory_order_acquire);
|
|
} else if (state_.compare_exchange_weak(
|
|
oldState,
|
|
oldState | kLockedFlag,
|
|
std::memory_order_acquire)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace detail
|
|
} // namespace folly
|