Skip to content

Commit f2d69e3

Browse files
authored
fix(ringchannel): backoff send with semaphore notification to prevent vcpu starvation (alibaba#1553)
1 parent b43e95a commit f2d69e3

1 file changed

Lines changed: 112 additions & 6 deletions

File tree

common/lockfree_queue.h

Lines changed: 112 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,12 +623,105 @@ namespace common {
623623
* LockfreeBatchMPMCRingQueue, or LockfreeSPSCRingQueue, with their own template
624624
* parameters.
625625
*/
626+
// Shared send-side backoff logic. Members (send_sem, send_waiters,
627+
// send_pending) are declared in each channel class; only the logic is shared.
628+
template <typename T>
629+
struct SendBackoff {
630+
static void notify_senders(photon::semaphore& send_sem,
631+
std::atomic<uint64_t>& send_waiters,
632+
std::atomic<uint64_t>& send_pending) {
633+
std::atomic_thread_fence(std::memory_order_seq_cst);
634+
auto cur_waiters = send_waiters.load(std::memory_order_seq_cst);
635+
if (cur_waiters == 0) return;
636+
auto sp = send_pending.load(std::memory_order_acquire);
637+
for (;;) {
638+
if (sp >= cur_waiters) {
639+
auto fresh = send_waiters.load(std::memory_order_relaxed);
640+
if (fresh <= cur_waiters) return;
641+
cur_waiters = fresh;
642+
continue;
643+
}
644+
if (send_pending.compare_exchange_weak(sp, sp + 1,
645+
std::memory_order_acq_rel,
646+
std::memory_order_acquire)) {
647+
send_sem.signal(1);
648+
return;
649+
}
650+
}
651+
}
652+
653+
template <typename Pause = ThreadPause, typename PushFn,
654+
typename std::enable_if<std::is_same<Pause, PhotonPause>::value, int>::type = 0>
655+
static void push_backoff(const T& x, PushFn push_fn, uint64_t yield_turn, uint64_t yield_usec,
656+
photon::semaphore& send_sem,
657+
std::atomic<uint64_t>& send_waiters,
658+
std::atomic<uint64_t>& send_pending) {
659+
if (!push_fn(x)) {
660+
send_waiters.fetch_add(1, std::memory_order_seq_cst);
661+
DEFER(send_waiters.fetch_sub(1, std::memory_order_seq_cst));
662+
Timeout yield_timeout(yield_usec);
663+
uint64_t yt = yield_turn;
664+
while (!push_fn(x)) {
665+
if (yt > 0 && !yield_timeout.expired()) {
666+
yt--;
667+
photon::thread_yield();
668+
} else {
669+
// wait for 100ms
670+
int r = send_sem.wait(1, 100UL * 1000);
671+
if (r == 0)
672+
send_pending.fetch_sub(1, std::memory_order_acq_rel);
673+
yt = yield_turn;
674+
yield_timeout.timeout(yield_usec);
675+
}
676+
}
677+
}
678+
}
679+
680+
template <typename Pause = ThreadPause, typename PushFn,
681+
typename std::enable_if<std::is_same<Pause, ThreadPause>::value, int>::type = 0>
682+
static void push_backoff(const T& x, PushFn push_fn, uint64_t yield_turn, uint64_t yield_usec,
683+
photon::semaphore&,
684+
std::atomic<uint64_t>&,
685+
std::atomic<uint64_t>&) {
686+
if (!push_fn(x)) {
687+
uint64_t yt = yield_turn;
688+
auto deadline = std::chrono::steady_clock::now() +
689+
std::chrono::microseconds(yield_usec);
690+
while (!push_fn(x)) {
691+
if (yt > 0 &&
692+
std::chrono::steady_clock::now() < deadline) {
693+
yt--;
694+
std::this_thread::yield();
695+
} else {
696+
std::this_thread::sleep_for(
697+
std::chrono::microseconds(yield_usec));
698+
yt = yield_turn;
699+
deadline = std::chrono::steady_clock::now() +
700+
std::chrono::microseconds(yield_usec);
701+
}
702+
}
703+
}
704+
}
705+
706+
template <typename Pause = ThreadPause, typename PushFn,
707+
typename std::enable_if<!std::is_same<Pause, PhotonPause>::value &&
708+
!std::is_same<Pause, ThreadPause>::value, int>::type = 0>
709+
static void push_backoff(const T& x, PushFn push_fn, uint64_t, uint64_t,
710+
photon::semaphore&, std::atomic<uint64_t>&,
711+
std::atomic<uint64_t>&) {
712+
while (!push_fn(x)) Pause::pause();
713+
}
714+
};
715+
626716
template <typename QueueType>
627717
class RingChannel : public QueueType {
628718
protected:
629719
photon::semaphore queue_sem;
630720
std::atomic<uint64_t> idler{0}; // # consumers in idle/wait
631721
std::atomic<uint64_t> pending{0}; // mirror of queue_sem.m_count
722+
photon::semaphore send_sem;
723+
std::atomic<uint64_t> send_waiters{0};
724+
std::atomic<uint64_t> send_pending{0};
632725
uint64_t default_yield_turn = 1024;
633726
uint64_t default_yield_usec = 1024;
634727

@@ -649,9 +742,9 @@ class RingChannel : public QueueType {
649742

650743
template <typename Pause = ThreadPause>
651744
void send(const T& x) {
652-
while (!push(x)) {
653-
Pause::pause();
654-
}
745+
SendBackoff<T>::template push_backoff<Pause>(x, [this](const T& v) { return push(v); },
746+
default_yield_turn, default_yield_usec,
747+
send_sem, send_waiters, send_pending);
655748
// Dekker barrier: ensure the prior push (mark.store release) is
656749
// ordered before the following idler load, paired with the seq_cst
657750
// RMW on `idler` in recv(). This guarantees that we cannot
@@ -683,7 +776,10 @@ class RingChannel : public QueueType {
683776
}
684777
T recv(uint64_t max_yield_turn, uint64_t max_yield_usec) {
685778
T x;
686-
if (pop(x)) return x;
779+
if (pop(x)) {
780+
SendBackoff<T>::notify_senders(send_sem, send_waiters, send_pending);
781+
return x;
782+
}
687783
// yield once if failed, so photon::now will be updated
688784
photon::thread_yield();
689785
// seq_cst on idler is the other half of the Dekker barrier (see send).
@@ -708,6 +804,7 @@ class RingChannel : public QueueType {
708804
yield_timeout.timeout(max_yield_usec);
709805
}
710806
}
807+
SendBackoff<T>::notify_senders(send_sem, send_waiters, send_pending);
711808
return x;
712809
}
713810
T recv() { return recv(default_yield_turn, default_yield_usec); }
@@ -738,6 +835,9 @@ class FlexRingChannel {
738835
photon::semaphore queue_sem;
739836
std::atomic<uint64_t> idler{0}; // # consumers in idle/wait
740837
std::atomic<uint64_t> pending{0}; // mirror of queue_sem.m_count
838+
photon::semaphore send_sem;
839+
std::atomic<uint64_t> send_waiters{0};
840+
std::atomic<uint64_t> send_pending{0};
741841
uint64_t default_yield_turn = 1024;
742842
uint64_t default_yield_usec = 1024;
743843

@@ -769,7 +869,9 @@ class FlexRingChannel {
769869

770870
template <typename Pause = ThreadPause>
771871
void send(const T& x) {
772-
queue->template send<Pause>(x);
872+
SendBackoff<T>::template push_backoff<Pause>(x, [this](const T& v) { return queue->push(v); },
873+
default_yield_turn, default_yield_usec,
874+
send_sem, send_waiters, send_pending);
773875
// Dekker barrier: ensure the prior push is ordered before the
774876
// following idler load, paired with the seq_cst RMW on `idler`
775877
// in recv().
@@ -798,7 +900,10 @@ class FlexRingChannel {
798900

799901
T recv(uint64_t max_yield_turn, uint64_t max_yield_usec) {
800902
T x;
801-
if (queue->pop(x)) return x;
903+
if (queue->pop(x)) {
904+
SendBackoff<T>::notify_senders(send_sem, send_waiters, send_pending);
905+
return x;
906+
}
802907
// yield once if failed, so photon::now will be updated
803908
photon::thread_yield();
804909
// seq_cst on idler is the other half of the Dekker barrier (see send).
@@ -823,6 +928,7 @@ class FlexRingChannel {
823928
yield_timeout.timeout(max_yield_usec);
824929
}
825930
}
931+
SendBackoff<T>::notify_senders(send_sem, send_waiters, send_pending);
826932
return x;
827933
}
828934

0 commit comments

Comments
 (0)