Commit 25374a6a authored by Lewis Baker's avatar Lewis Baker Committed by Facebook Github Bot

Add folly::CancellationToken

Summary:
Adds a general-purpose CancellationToken abstraction that can be used to build APIs that allow the caller to pass in a CancellationToken that the caller can later use to communicate a request to cancel the operation.

An operation can either poll for cancellation by calling the isCancellationRequested() method or can register for notification of a cancellation request by attaching a callback to the CancellationToken using the CancellationCallback class.

The caller first constructs a CancellationSource, which allows them to request cancellation, and uses the CancellationSource to obtain CancellationToken objects which it can then pass into cancellable functions.

This implementation is based on the reference implementation for the  interrupt_token/stop_token abstraction proposed for C++20.

```
void polling_operation(folly::CancellationToken ct)
{
  while (!ct.isCancellationRequested())
  {
    do_work();
  }
}

void blocking_operation(folly::CancellationToken ct)
{
  folly::Baton baton;

  // Register a callback.
  folly::CancellationCallback cb{ct, [&] { baton.post(); }};

  // Blocks until cancelled.
  baton.wait();
}

void caller()
{
  CancellationSource src;
  std::thread t1{ [&] {
    polling_operation(src.getToken());
    } };
  std::thread t2{ [&] {
    blocking_operation(src.getToken());
    } };
  std::this_thread::sleep_for(1s);
  src.requestCancellation();
  t1.join();
  t2.join();
}
```

Reviewed By: andriigrynenko

Differential Revision: D10522066

fbshipit-source-id: 11ad3c104eda6650d11081485509981c9b1ea110
parent 51635047
/*
* Copyright 2018-present Facebook, Inc.
*
* 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 <algorithm>
#include <cstdint>
#include <limits>
#include <utility>
#include <glog/logging.h>
namespace folly {
namespace detail {
// Internal cancellation state object.
class CancellationState {
public:
FOLLY_NODISCARD static CancellationStateSourcePtr create();
private:
// Constructed initially with a CancellationSource reference count of 1.
CancellationState() noexcept;
~CancellationState();
friend struct CancellationStateTokenDeleter;
friend struct CancellationStateSourceDeleter;
void removeTokenReference() noexcept;
void removeSourceReference() noexcept;
public:
FOLLY_NODISCARD CancellationStateTokenPtr addTokenReference() noexcept;
FOLLY_NODISCARD CancellationStateSourcePtr addSourceReference() noexcept;
bool tryAddCallback(
CancellationCallback* callback,
bool incrementRefCountIfSuccessful) noexcept;
void removeCallback(CancellationCallback* callback) noexcept;
bool isCancellationRequested() const noexcept;
bool canBeCancelled() const noexcept;
// Request cancellation.
// Return 'true' if cancellation had already been requested.
// Return 'false' if this was the first thread to request
// cancellation.
bool requestCancellation() noexcept;
private:
void lock() noexcept;
void unlock() noexcept;
void unlockAndIncrementTokenCount() noexcept;
void unlockAndDecrementTokenCount() noexcept;
bool tryLockAndCancelUnlessCancelled() noexcept;
template <typename Predicate>
bool tryLock(Predicate predicate) noexcept;
static bool canBeCancelled(std::uint64_t state) noexcept;
static bool isCancellationRequested(std::uint64_t state) noexcept;
static bool isLocked(std::uint64_t state) noexcept;
static constexpr std::uint64_t kCancellationRequestedFlag = 1;
static constexpr std::uint64_t kLockedFlag = 2;
static constexpr std::uint64_t kTokenReferenceCountIncrement = 4;
static constexpr std::uint64_t kSourceReferenceCountIncrement =
std::uint64_t(1) << 33u;
static constexpr std::uint64_t kTokenReferenceCountMask =
(kSourceReferenceCountIncrement - 1u) -
(kTokenReferenceCountIncrement - 1u);
static constexpr std::uint64_t kSourceReferenceCountMask =
std::numeric_limits<std::uint64_t>::max() -
(kSourceReferenceCountIncrement - 1u);
// Bit 0 - Cancellation Requested
// Bit 1 - Locked Flag
// Bits 2-32 - Token reference count (max ~2 billion)
// Bits 33-63 - Source reference count (max ~2 billion)
std::atomic<std::uint64_t> state_;
CancellationCallback* head_;
std::thread::id signallingThreadId_;
};
inline void CancellationStateTokenDeleter::operator()(
CancellationState* state) noexcept {
state->removeTokenReference();
}
inline void CancellationStateSourceDeleter::operator()(
CancellationState* state) noexcept {
state->removeSourceReference();
}
} // namespace detail
inline CancellationToken::CancellationToken(
const CancellationToken& other) noexcept
: state_() {
if (other.state_) {
state_ = other.state_->addTokenReference();
}
}
inline CancellationToken::CancellationToken(CancellationToken&& other) noexcept
: state_(std::move(other.state_)) {}
inline CancellationToken& CancellationToken::operator=(
const CancellationToken& other) noexcept {
if (state_ != other.state_) {
CancellationToken temp{other};
swap(temp);
}
return *this;
}
inline CancellationToken& CancellationToken::operator=(
CancellationToken&& other) noexcept {
state_ = std::move(other.state_);
return *this;
}
inline bool CancellationToken::isCancellationRequested() const noexcept {
return state_ != nullptr && state_->isCancellationRequested();
}
inline bool CancellationToken::canBeCancelled() const noexcept {
return state_ != nullptr && state_->canBeCancelled();
}
inline void CancellationToken::swap(CancellationToken& other) noexcept {
std::swap(state_, other.state_);
}
inline CancellationToken::CancellationToken(
detail::CancellationStateTokenPtr state) noexcept
: state_(std::move(state)) {}
inline bool operator==(
const CancellationToken& a,
const CancellationToken& b) noexcept {
return a.state_ == b.state_;
}
inline bool operator!=(
const CancellationToken& a,
const CancellationToken& b) noexcept {
return !(a == b);
}
inline CancellationSource::CancellationSource()
: state_(detail::CancellationState::create()) {}
inline CancellationSource::CancellationSource(
const CancellationSource& other) noexcept
: state_() {
if (other.state_) {
state_ = other.state_->addSourceReference();
}
}
inline CancellationSource::CancellationSource(
CancellationSource&& other) noexcept
: state_(std::move(other.state_)) {}
inline CancellationSource& CancellationSource::operator=(
const CancellationSource& other) noexcept {
if (state_ != other.state_) {
CancellationSource temp{other};
swap(temp);
}
return *this;
}
inline CancellationSource& CancellationSource::operator=(
CancellationSource&& other) noexcept {
state_ = std::move(other.state_);
return *this;
}
inline bool CancellationSource::isCancellationRequested() const noexcept {
return state_ != nullptr && state_->isCancellationRequested();
}
inline bool CancellationSource::canBeCancelled() const noexcept {
return state_ != nullptr;
}
inline CancellationToken CancellationSource::getToken() const noexcept {
if (state_ != nullptr) {
return CancellationToken{state_->addTokenReference()};
}
return CancellationToken{};
}
inline bool CancellationSource::requestCancellation() const noexcept {
if (state_ != nullptr) {
return state_->requestCancellation();
}
return false;
}
inline void CancellationSource::swap(CancellationSource& other) noexcept {
std::swap(state_, other.state_);
}
template <
typename Callable,
std::enable_if_t<
std::is_constructible<CancellationCallback::VoidFunction, Callable>::
value,
int>>
inline CancellationCallback::CancellationCallback(
CancellationToken&& ct,
Callable&& callable)
: next_(nullptr),
prevNext_(nullptr),
state_(nullptr),
callback_(static_cast<Callable&&>(callable)),
destructorHasRunInsideCallback_(nullptr),
callbackCompleted_(false) {
if (ct.state_ != nullptr && ct.state_->tryAddCallback(this, false)) {
state_ = ct.state_.release();
}
}
template <
typename Callable,
std::enable_if_t<
std::is_constructible<CancellationCallback::VoidFunction, Callable>::
value,
int>>
inline CancellationCallback::CancellationCallback(
const CancellationToken& ct,
Callable&& callable)
: next_(nullptr),
prevNext_(nullptr),
state_(nullptr),
callback_(static_cast<Callable&&>(callable)),
destructorHasRunInsideCallback_(nullptr),
callbackCompleted_(false) {
if (ct.state_ != nullptr && ct.state_->tryAddCallback(this, true)) {
state_ = ct.state_.get();
}
}
inline CancellationCallback::~CancellationCallback() {
if (state_ != nullptr) {
state_->removeCallback(this);
}
}
inline void CancellationCallback::invokeCallback() noexcept {
// Invoke within a noexcept context so that we std::terminate() if it throws.
callback_();
}
namespace detail {
inline CancellationStateSourcePtr CancellationState::create() {
return CancellationStateSourcePtr{new CancellationState()};
}
inline CancellationState::CancellationState() noexcept
: state_(kSourceReferenceCountIncrement),
head_(nullptr),
signallingThreadId_() {}
inline CancellationStateTokenPtr
CancellationState::addTokenReference() noexcept {
state_.fetch_add(kTokenReferenceCountIncrement, std::memory_order_relaxed);
return CancellationStateTokenPtr{this};
}
inline void CancellationState::removeTokenReference() noexcept {
const auto oldState = state_.fetch_sub(
kTokenReferenceCountIncrement, std::memory_order_acq_rel);
DCHECK(
(oldState & kTokenReferenceCountMask) >= kTokenReferenceCountIncrement);
if (oldState < (2 * kTokenReferenceCountIncrement)) {
delete this;
}
}
inline CancellationStateSourcePtr
CancellationState::addSourceReference() noexcept {
state_.fetch_add(kSourceReferenceCountIncrement, std::memory_order_relaxed);
return CancellationStateSourcePtr{this};
}
inline void CancellationState::removeSourceReference() noexcept {
const auto oldState = state_.fetch_sub(
kSourceReferenceCountIncrement, std::memory_order_acq_rel);
DCHECK(
(oldState & kSourceReferenceCountMask) >= kSourceReferenceCountIncrement);
if (oldState <
(kSourceReferenceCountIncrement + kTokenReferenceCountIncrement)) {
delete this;
}
}
inline bool CancellationState::isCancellationRequested() const noexcept {
return isCancellationRequested(state_.load(std::memory_order_acquire));
}
inline bool CancellationState::canBeCancelled() const noexcept {
return canBeCancelled(state_.load(std::memory_order_acquire));
}
inline bool CancellationState::canBeCancelled(std::uint64_t state) noexcept {
// Can be cancelled if there is at least one CancellationSource ref-count
// or if cancellation has been requested.
return (state >= kSourceReferenceCountIncrement) ||
isCancellationRequested(state);
}
inline bool CancellationState::isCancellationRequested(
std::uint64_t state) noexcept {
return (state & kCancellationRequestedFlag) != 0;
}
inline bool CancellationState::isLocked(std::uint64_t state) noexcept {
return (state & kLockedFlag) != 0;
}
} // namespace detail
} // namespace folly
/*
* Copyright 2018-present Facebook, Inc.
*
* 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
/*
* Copyright 2018-present Facebook, Inc.
*
* 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/CppAttributes.h>
#include <folly/Function.h>
#include <atomic>
#include <memory>
#include <thread>
#include <type_traits>
namespace folly {
class CancellationCallback;
class CancellationSource;
namespace detail {
class CancellationState;
struct CancellationStateTokenDeleter {
void operator()(CancellationState*) noexcept;
};
struct CancellationStateSourceDeleter {
void operator()(CancellationState*) noexcept;
};
using CancellationStateTokenPtr =
std::unique_ptr<CancellationState, CancellationStateTokenDeleter>;
using CancellationStateSourcePtr =
std::unique_ptr<CancellationState, CancellationStateSourceDeleter>;
} // namespace detail
// A CancellationToken is an object that can be passed into an function or
// operation that allows the caller to later request that the operation be
// cancelled.
//
// A CancellationToken object can be obtained by calling the .getToken()
// method on a CancellationSource or by copying another CancellationToken
// object. All CancellationToken objects obtained from the same original
// CancellationSource object all reference the same underlying cancellation
// state and will all be cancelled together.
//
// If your function needs to be cancellable but does not need to request
// cancellation then you should take a CancellationToken as a parameter.
// If your function needs to be able to request cancellation then you
// should instead take a CancellationSource as a parameter.
class CancellationToken {
public:
// Constructs to a token that can never be cancelled.
//
// Pass a default-constructed CancellationToken into an operation that
// you never intend to cancel. These objects are very cheap to create.
CancellationToken() noexcept = default;
// Construct a copy of the token that shares the same underlying state.
CancellationToken(const CancellationToken& other) noexcept;
CancellationToken(CancellationToken&& other) noexcept;
CancellationToken& operator=(const CancellationToken& other) noexcept;
CancellationToken& operator=(CancellationToken&& other) noexcept;
// Query whether someone has called .requestCancellation() on an instance
// of CancellationSource object associated with this CancellationToken.
bool isCancellationRequested() const noexcept;
// Query whether this CancellationToken can ever have cancellation requested
// on it.
//
// This will return false if the CancellationToken is not associated with a
// CancellationSource object. eg. because the CancellationToken was
// default-constructed, has been moved-from or because the last
// CancellationSource object associated with the underlying cancellation state
// has been destroyed and the operation has not yet been cancelled and so
// never will be.
//
// Implementations of operations may be able to take more efficient code-paths
// if they know they can never be cancelled.
bool canBeCancelled() const noexcept;
void swap(CancellationToken& other) noexcept;
friend bool operator==(
const CancellationToken& a,
const CancellationToken& b) noexcept;
private:
friend class CancellationCallback;
friend class CancellationSource;
explicit CancellationToken(detail::CancellationStateTokenPtr state) noexcept;
detail::CancellationStateTokenPtr state_;
};
bool operator==(
const CancellationToken& a,
const CancellationToken& b) noexcept;
bool operator!=(
const CancellationToken& a,
const CancellationToken& b) noexcept;
// A CancellationSource object provides the ability to request cancellation of
// operations that an associated CancellationToken was passed to.
//
// Example usage:
// CancellationSource cs;
// Future<void> f = startSomeOperation(cs.getToken());
//
// // Later...
// cs.requestCancellation();
class CancellationSource {
public:
// Construct to a new, independent cancellation source.
CancellationSource();
// Construct a new reference to the same underlying cancellation state.
//
// Either the original or the new copy can be used to request cancellation
// of associated work.
CancellationSource(const CancellationSource& other) noexcept;
// This leaves 'other' in an empty state where 'requestCancellation()' is a
// no-op and 'canBeCancelled()' returns false.
CancellationSource(CancellationSource&& other) noexcept;
CancellationSource& operator=(const CancellationSource& other) noexcept;
CancellationSource& operator=(CancellationSource&& other) noexcept;
// Query if cancellation has already been requested on this CancellationSource
// or any other CancellationSource object copied from the same original
// CancellationSource object.
bool isCancellationRequested() const noexcept;
// Query if cancellation can be requested through this CancellationSource
// object. This will only return false if the CancellationSource object has
// been moved-from.
bool canBeCancelled() const noexcept;
// Obtain a CancellationToken linked to this CancellationSource.
//
// This token can be passed into cancellable operations to allow the caller
// to later request cancellation of that operation.
CancellationToken getToken() const noexcept;
// Request cancellation of work associated with this CancellationSource.
//
// This will ensure subsequent calls to isCancellationRequested() on any
// CancellationSource or CancellationToken object associated with the same
// underlying cancellation state to return true.
//
// If this is the first call to requestCancellation() on any
// CancellationSource object with the same underlying state then this call
// will also execute the callbacks associated with any CancellationCallback
// objects that were constructed with an associated CancellationToken.
//
// Note that it is possible that another thread may be concurrently
// registering a callback with CancellationCallback. This method guarantees
// that either this thread will see the callback registration and will
// ensure that the callback is called, or the CancellationCallback constructor
// will see the cancellation-requested signal and will execute the callback
// inline inside the constructor.
//
// Returns the previous state of 'isCancellationRequested()'. i.e.
// - 'true' if cancellation had previously been requested.
// - 'false' if this was the first call to request cancellation.
bool requestCancellation() const noexcept;
void swap(CancellationSource& other) noexcept;
friend bool operator==(
const CancellationSource& a,
const CancellationSource& b) noexcept;
private:
detail::CancellationStateSourcePtr state_;
};
bool operator==(
const CancellationSource& a,
const CancellationSource& b) noexcept;
bool operator!=(
const CancellationSource& a,
const CancellationSource& b) noexcept;
class CancellationCallback {
using VoidFunction = folly::Function<void()>;
public:
// Constructing a CancellationCallback object registers the callback
// with the specified CancellationToken such that the callback will be
// executed if the corresponding CancellationSource object has the
// requestCancellation() method called on it.
//
// If the CancellationToken object already had cancellation requested
// then the callback will be executed inline on the current thread before
// the constructor returns. Otherwise, the callback will be executed on
// in the execution context of the first thread to call requestCancellation()
// on a corresponding CancellationSource.
//
// The callback object must not throw any unhandled exceptions. Doing so
// will result in the program terminating via std::terminate().
template <
typename Callable,
std::enable_if_t<
std::is_constructible<VoidFunction, Callable>::value,
int> = 0>
CancellationCallback(CancellationToken&& ct, Callable&& callable);
template <
typename Callable,
std::enable_if_t<
std::is_constructible<VoidFunction, Callable>::value,
int> = 0>
CancellationCallback(const CancellationToken& ct, Callable&& callable);
// Deregisters the callback from the CancellationToken.
//
// If cancellation has been requested concurrently on another thread and the
// callback is currently executing then the destructor will block until after
// the callback has returned (otherwise it might be left with a dangling
// reference).
//
// You should generally try to implement your callback functions to be lock
// free to avoid deadlocks between the callback executing and the
// CancellationCallback destructor trying to deregister the callback.
//
// If the callback has not started executing yet then the callback will be
// deregistered from the CancellationToken before the destructor completes.
//
// Once the destructor returns you can be guaranteed that the callback will
// not be called by a subsequent call to 'requestCancellation()' on a
// CancellationSource associated with the CancellationToken passed to the
// constructor.
~CancellationCallback();
// Not copyable/movable
CancellationCallback(const CancellationCallback&) = delete;
CancellationCallback(CancellationCallback&&) = delete;
CancellationCallback& operator=(const CancellationCallback&) = delete;
CancellationCallback& operator=(CancellationCallback&&) = delete;
private:
friend class detail::CancellationState;
void invokeCallback() noexcept;
CancellationCallback* next_;
// Pointer to the pointer that points to this node in the linked list.
// This could be the 'next_' of a previous CancellationCallback or could
// be the 'head_' pointer of the CancellationState.
// If this node is inserted in the list then this will be non-null.
CancellationCallback** prevNext_;
detail::CancellationState* state_;
VoidFunction callback_;
// Pointer to a flag stored on the stack of the caller to invokeCallback()
// that is used to indicate to the caller of invokeCallback() that the
// destructor has run and it is no longer valid to access the callback
// object.
bool* destructorHasRunInsideCallback_;
// Flag used to signal that the callback has completed executing on another
// thread and it is now safe to exit the destructor.
std::atomic<bool> callbackCompleted_;
};
} // namespace folly
#include <folly/CancellationToken-inl.h>
/*
* Copyright 2018-present Facebook, Inc.
*
* 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/portability/GTest.h>
#include <folly/synchronization/Baton.h>
#include <chrono>
#include <thread>
using namespace folly;
using namespace std::literals::chrono_literals;
TEST(CancellationTokenTest, DefaultCancellationTokenIsNotCancellable) {
CancellationToken t;
EXPECT_FALSE(t.isCancellationRequested());
EXPECT_FALSE(t.canBeCancelled());
CancellationToken tCopy = t;
EXPECT_FALSE(tCopy.isCancellationRequested());
EXPECT_FALSE(tCopy.canBeCancelled());
CancellationToken tMoved = std::move(t);
EXPECT_FALSE(tMoved.isCancellationRequested());
EXPECT_FALSE(tMoved.canBeCancelled());
}
TEST(CancellationTokenTest, Polling) {
CancellationSource src;
EXPECT_FALSE(src.isCancellationRequested());
EXPECT_TRUE(src.canBeCancelled());
CancellationToken token = src.getToken();
EXPECT_FALSE(token.isCancellationRequested());
EXPECT_TRUE(token.canBeCancelled());
CancellationToken tokenCopy = token;
EXPECT_FALSE(tokenCopy.isCancellationRequested());
EXPECT_TRUE(tokenCopy.canBeCancelled());
src.requestCancellation();
EXPECT_TRUE(token.isCancellationRequested());
EXPECT_TRUE(tokenCopy.isCancellationRequested());
}
TEST(CancellationTokenTest, MultiThreadedPolling) {
CancellationSource src;
std::thread t1{[t = src.getToken()] {
while (!t.isCancellationRequested()) {
std::this_thread::yield();
}
}};
src.requestCancellation();
t1.join();
}
TEST(CancellationTokenTest, TokenIsNotCancellableOnceLastSourceIsDestroyed) {
CancellationToken token;
{
CancellationSource src;
token = src.getToken();
{
CancellationSource srcCopy1;
CancellationSource srcCopy2;
EXPECT_TRUE(token.canBeCancelled());
}
EXPECT_TRUE(token.canBeCancelled());
}
EXPECT_FALSE(token.canBeCancelled());
}
TEST(
CancellationTokenTest,
TokenRemainsCancellableEvenOnceLastSourceIsDestroyed) {
CancellationToken token;
{
CancellationSource src;
token = src.getToken();
{
CancellationSource srcCopy1;
CancellationSource srcCopy2;
EXPECT_TRUE(token.canBeCancelled());
}
EXPECT_TRUE(token.canBeCancelled());
src.requestCancellation();
}
EXPECT_TRUE(token.canBeCancelled());
EXPECT_TRUE(token.isCancellationRequested());
}
TEST(CancellationTokenTest, CallbackRegistration) {
CancellationSource src;
bool callbackExecuted = false;
CancellationCallback cb{src.getToken(), [&] { callbackExecuted = true; }};
EXPECT_FALSE(callbackExecuted);
src.requestCancellation();
EXPECT_TRUE(callbackExecuted);
}
TEST(CancellationTokenTest, CallbackExecutesImmediatelyIfAlreadyCancelled) {
CancellationSource src;
src.requestCancellation();
bool callbackExecuted = false;
CancellationCallback cb{src.getToken(), [&] { callbackExecuted = true; }};
EXPECT_TRUE(callbackExecuted);
}
TEST(CancellationTokenTest, CallbackShouldNotBeExecutedMultipleTimes) {
CancellationSource src;
int callbackExecutionCount = 0;
CancellationCallback cb{src.getToken(), [&] { ++callbackExecutionCount; }};
src.requestCancellation();
EXPECT_EQ(1, callbackExecutionCount);
src.requestCancellation();
EXPECT_EQ(1, callbackExecutionCount);
}
TEST(CancellationTokenTest, RegisterMultipleCallbacks) {
CancellationSource src;
bool executed1 = false;
CancellationCallback cb1{src.getToken(), [&] { executed1 = true; }};
bool executed2 = false;
CancellationCallback cb2{src.getToken(), [&] { executed2 = true; }};
EXPECT_FALSE(executed1);
EXPECT_FALSE(executed2);
src.requestCancellation();
EXPECT_TRUE(executed1);
EXPECT_TRUE(executed2);
}
TEST(CancellationTokenTest, DeregisteredCallbacksDontExecute) {
CancellationSource src;
bool executed1 = false;
bool executed2 = false;
CancellationCallback cb1{src.getToken(), [&] { executed1 = true; }};
{
CancellationCallback cb2{src.getToken(), [&] { executed2 = true; }};
}
src.requestCancellation();
EXPECT_TRUE(executed1);
EXPECT_FALSE(executed2);
}
TEST(CancellationTokenTest, CallbackThatDeregistersItself) {
CancellationSource src;
// Check that this doesn't deadlock when a callback tries to deregister
// itself from within the callback.
folly::Optional<CancellationCallback> callback;
callback.emplace(src.getToken(), [&] { callback.clear(); });
src.requestCancellation();
}
TEST(CancellationTokenTest, ManyCallbacks) {
// This test checks that the CancellationSource internal state is able to
// grow to accommodate a large number of callbacks and that there are no
// memory leaks when it's all eventually destroyed.
CancellationSource src;
auto addLotsOfCallbacksAndWait = [t = src.getToken()] {
int counter = 0;
std::vector<std::unique_ptr<CancellationCallback>> callbacks;
for (int i = 0; i < 100; ++i) {
callbacks.push_back(
std::make_unique<CancellationCallback>(t, [&] { ++counter; }));
}
Baton<> baton;
CancellationCallback cb{t, [&] { baton.post(); }};
baton.wait();
};
std::thread t1{addLotsOfCallbacksAndWait};
std::thread t2{addLotsOfCallbacksAndWait};
std::thread t3{addLotsOfCallbacksAndWait};
std::thread t4{addLotsOfCallbacksAndWait};
src.requestCancellation();
t1.join();
t2.join();
t3.join();
t4.join();
}
TEST(CancellationTokenTest, ManyConcurrentCallbackAddRemove) {
auto runTest = [](CancellationToken ct) {
auto cb = [] { std::this_thread::sleep_for(1ms); };
while (!ct.isCancellationRequested()) {
CancellationCallback cb1{ct, cb};
CancellationCallback cb2{ct, cb};
CancellationCallback cb3{ct, cb};
CancellationCallback cb5{ct, cb};
CancellationCallback cb6{ct, cb};
CancellationCallback cb7{ct, cb};
CancellationCallback cb8{ct, cb};
}
};
CancellationSource src;
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([&, t = src.getToken()] { runTest(t); });
}
std::this_thread::sleep_for(1s);
src.requestCancellation();
for (auto& t : threads) {
t.join();
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment