Commit d43e710c authored by Phil Willoughby's avatar Phil Willoughby Committed by Facebook Github Bot 6

Convert a polling loop to a futex wait

Summary:Add a new method to MPMCQueue:
```
template <class Clock, typename... Args>
  bool tryWriteUntil(const std::chrono::time_point<Clock>& when,
                     Args&&... args) noexcept
```
This allows you to write producers which terminate reliably in the absence of consumers.

Returns `true` if `args` was enqueued, `false` otherwise.

`Clock` must be one of the types supported by the underlying call to `folly::detail::Futex::futexWaitUntil`; at time of writing these are `std::chrono::steady_clock` and `std::chrono::system_clock`.

Reviewed By: nbronson

Differential Revision: D2895574

fb-gh-sync-id: bdfabcd043191c149f1271e30ffc28476cc8a36e
shipit-source-id: bdfabcd043191c149f1271e30ffc28476cc8a36e
parent f509d73d
......@@ -284,6 +284,21 @@ class MPMCQueue : boost::noncopyable {
}
}
template <class Clock, typename... Args>
bool tryWriteUntil(const std::chrono::time_point<Clock>& when,
Args&&... args) noexcept {
uint64_t ticket;
if (tryObtainPromisedPushTicketUntil(ticket, when)) {
// we have pre-validated that the ticket won't block, or rather that
// it won't block longer than it takes another thread to dequeue an
// element from the slot it identifies.
enqueueWithTicket(ticket, std::forward<Args>(args)...);
return true;
} else {
return false;
}
}
/// If the queue is not full, enqueues and returns true, otherwise
/// returns false. Unlike write this method can be blocked by another
/// thread, specifically a read that has linearized (been assigned
......@@ -471,6 +486,28 @@ class MPMCQueue : boost::noncopyable {
}
}
/// Tries until when to obtain a push ticket for which
/// SingleElementQueue::enqueue won't block. Returns true on success, false
/// on failure.
/// ticket is filled on success AND failure.
template <class Clock>
bool tryObtainPromisedPushTicketUntil(
uint64_t& ticket, const std::chrono::time_point<Clock>& when) noexcept {
bool deadlineReached = false;
while (!deadlineReached) {
if (tryObtainPromisedPushTicket(ticket)) {
return true;
}
// ticket is a blocking ticket until the preceding ticket has been
// processed: wait until this ticket's turn arrives. We have not reserved
// this ticket so we will have to re-attempt to get a non-blocking ticket
// if we wake up before we time-out.
deadlineReached = !slots_[idx(ticket)].tryWaitForEnqueueTurnUntil(
turn(ticket), pushSpinCutoff_, (ticket % kAdaptationFreq) == 0, when);
}
return false;
}
/// Tries to obtain a push ticket which can be satisfied if all
/// in-progress pops complete. This function does not block, but
/// blocking may be required when using the returned ticket if some
......@@ -482,6 +519,7 @@ class MPMCQueue : boost::noncopyable {
auto numPops = popTicket_.load(std::memory_order_acquire); // B
// n will be negative if pops are pending
int64_t n = numPushes - numPops;
rv = numPushes;
if (n >= static_cast<ssize_t>(capacity_)) {
// Full, linearize at B. We don't need to recheck the read we
// performed at A, because if numPushes was stale at B then the
......@@ -489,7 +527,6 @@ class MPMCQueue : boost::noncopyable {
return false;
}
if (pushTicket_.compare_exchange_strong(numPushes, numPushes + 1)) {
rv = numPushes;
return true;
}
}
......@@ -597,7 +634,7 @@ struct SingleElementQueue {
template <typename = typename std::enable_if<
(folly::IsRelocatable<T>::value &&
boost::has_nothrow_constructor<T>::value) ||
std::is_nothrow_constructible<T,T&&>::value>::type>
std::is_nothrow_constructible<T, T&&>::value>::type>
void enqueue(const uint32_t turn,
Atom<uint32_t>& spinCutoff,
const bool updateSpinCutoff,
......@@ -611,6 +648,20 @@ struct SingleElementQueue {
ImplByMove, ImplByRelocation>::type());
}
/// Waits until either:
/// 1: the dequeue turn preceding the given enqueue turn has arrived
/// 2: the given deadline has arrived
/// Case 1 returns true, case 2 returns false.
template <class Clock>
bool tryWaitForEnqueueTurnUntil(
const uint32_t turn,
Atom<uint32_t>& spinCutoff,
const bool updateSpinCutoff,
const std::chrono::time_point<Clock>& when) noexcept {
return sequencer_.tryWaitForTurn(
turn * 2, spinCutoff, updateSpinCutoff, &when);
}
bool mayEnqueue(const uint32_t turn) const noexcept {
return sequencer_.isTurn(turn * 2);
}
......
......@@ -82,10 +82,10 @@ struct TurnSequencer {
/// See tryWaitForTurn
/// Requires that `turn` is not a turn in the past.
void waitForTurn(const uint32_t turn,
Atom<uint32_t>& spinCutoff,
const bool updateSpinCutoff) noexcept {
Atom<uint32_t>& spinCutoff,
const bool updateSpinCutoff) noexcept {
bool success = tryWaitForTurn(turn, spinCutoff, updateSpinCutoff);
(void) success;
(void)success;
assert(success);
}
......@@ -99,9 +99,15 @@ struct TurnSequencer {
/// before blocking and will adjust spinCutoff based on the results,
/// otherwise it will spin for at most spinCutoff spins.
/// Returns true if the wait succeeded, false if the turn is in the past
/// or the absTime time value is not nullptr and is reached before the turn
/// arrives
template <class Clock = std::chrono::steady_clock,
class Duration = typename Clock::duration>
bool tryWaitForTurn(const uint32_t turn,
Atom<uint32_t>& spinCutoff,
const bool updateSpinCutoff) noexcept {
Atom<uint32_t>& spinCutoff,
const bool updateSpinCutoff,
const std::chrono::time_point<Clock, Duration>* absTime =
nullptr) noexcept {
uint32_t prevThresh = spinCutoff.load(std::memory_order_relaxed);
const uint32_t effectiveSpinCutoff =
updateSpinCutoff || prevThresh == 0 ? kMaxSpins : prevThresh;
......@@ -142,7 +148,15 @@ struct TurnSequencer {
continue;
}
}
state_.futexWait(new_state, futexChannel(turn));
if (absTime) {
auto futexResult =
state_.futexWaitUntil(new_state, *absTime, futexChannel(turn));
if (futexResult == FutexResult::TIMEDOUT) {
return false;
}
} else {
state_.futexWait(new_state, futexChannel(turn));
}
}
if (updateSpinCutoff || prevThresh == 0) {
......@@ -179,9 +193,9 @@ struct TurnSequencer {
while (true) {
assert(state == encode(turn << kTurnShift, decodeMaxWaitersDelta(state)));
uint32_t max_waiter_delta = decodeMaxWaitersDelta(state);
uint32_t new_state = encode(
(turn + 1) << kTurnShift,
max_waiter_delta == 0 ? 0 : max_waiter_delta - 1);
uint32_t new_state =
encode((turn + 1) << kTurnShift,
max_waiter_delta == 0 ? 0 : max_waiter_delta - 1);
if (state_.compare_exchange_strong(state, new_state)) {
if (max_waiter_delta != 0) {
state_.futexWake(std::numeric_limits<int>::max(),
......@@ -227,9 +241,7 @@ struct TurnSequencer {
/// Returns the bitmask to pass futexWait or futexWake when communicating
/// about the specified turn
int futexChannel(uint32_t turn) const noexcept {
return 1 << (turn & 31);
}
int futexChannel(uint32_t turn) const noexcept { return 1 << (turn & 31); }
uint32_t decodeCurrentSturn(uint32_t state) const noexcept {
return state & ~kWaitersMask;
......@@ -240,7 +252,7 @@ struct TurnSequencer {
}
uint32_t encode(uint32_t currentSturn, uint32_t maxWaiterD) const noexcept {
return currentSturn | std::min(uint32_t{ kWaitersMask }, maxWaiterD);
return currentSturn | std::min(uint32_t{kWaitersMask}, maxWaiterD);
}
};
......
This diff is collapsed.
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