This is an automated email from the ASF dual-hosted git repository.

chenBright pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brpc.git


The following commit(s) were added to refs/heads/master by this push:
     new 09f36c53 Fix lost wakeup in ParkingLot waiter check (#3539)
09f36c53 is described below

commit 09f36c53b5d36b0c86c91dad3aeba8c736fa98fa
Author: Bright Chen <[email protected]>
AuthorDate: Tue Sep 15 21:54:37 2026 +0800

    Fix lost wakeup in ParkingLot waiter check (#3539)
---
 src/bthread/parking_lot.h             |  60 ++++-
 test/bthread_parking_lot_unittest.cpp | 473 ++++++++++++++++++++++++++++++++++
 2 files changed, 526 insertions(+), 7 deletions(-)

diff --git a/src/bthread/parking_lot.h b/src/bthread/parking_lot.h
index bd8c2c99..ffa365ae 100644
--- a/src/bthread/parking_lot.h
+++ b/src/bthread/parking_lot.h
@@ -49,10 +49,46 @@ public:
 
     // Wake up at most `num_task' workers.
     // Returns #workers woken up.
+    //
+    // When _no_signal_when_no_waiter is enabled, signal() and wait() form a
+    // Dekker-style synchronization. Both sides MUST preserve StoreLoad order:
+    //
+    //   signal(): _pending_signal += num_task * 2;  load(_waiter_num)
+    //   wait():   _waiter_num += 1;                 load(_pending_signal)
+    //
+    // memory_order_release is NOT enough for the signal side, since it does 
not
+    // prevent the following load from being reordered before the store. Once 
the
+    // order is broken, each side may observe the other side's old value, which
+    // ends up with a lost wakeup: signal() sees no waiter and skips 
futex_wake(),
+    // while the waiter sees no new signal and goes to sleep.
+    //
+    // With the matching seq_cst fences below, the StoreLoad order is enforced
+    // on both sides:
+    //
+    //   signaler                         waiter
+    //   --------                         ------
+    //   _pending_signal += num_task * 2
+    //   seq_cst fence
+    //                                    _waiter_num += 1
+    //                                    seq_cst fence
+    //   load(_waiter_num)
+    //     |
+    //     +-- > 0: futex_wake() -------> waiter is woken, or has not slept yet
+    //     |
+    //     +-- == 0: return
+    //                                    load(_pending_signal)
+    //                                    -> sees the new value; does not sleep
+    //
+    // Thus, signal() either observes a published waiter and wakes it, or the
+    // waiter observes the published signal before it can sleep.
     int signal(int num_task) {
         _pending_signal.fetch_add((num_task << 1), 
butil::memory_order_release);
-        if (_no_signal_when_no_waiter && 
_waiter_num.load(butil::memory_order_relaxed) == 0) {
-            return 0;
+        if (_no_signal_when_no_waiter) {
+            // Matching StoreLoad fence for the waiter-side fence below.
+            butil::atomic_thread_fence(butil::memory_order_seq_cst);
+            if (_waiter_num.load(butil::memory_order_relaxed) == 0) {
+                return 0;
+            }
         }
         return futex_wake_private(&_pending_signal, num_task);
     }
@@ -69,13 +105,23 @@ public:
             // Fast path, no need to futex_wait.
             return;
         }
-        if (_no_signal_when_no_waiter) {
-            _waiter_num.fetch_add(1, butil::memory_order_relaxed);
+        if (!_no_signal_when_no_waiter) {
+            futex_wait_private(&_pending_signal, expected_state.val, NULL);
+            return;
         }
-        futex_wait_private(&_pending_signal, expected_state.val, nullptr);
-        if (_no_signal_when_no_waiter) {
-            _waiter_num.fetch_sub(1, butil::memory_order_relaxed);
+
+        _waiter_num.fetch_add(1, butil::memory_order_relaxed);
+        // Matching StoreLoad fence for the signal-side fence above. It 
publishes
+        // this waiter before _pending_signal is checked below.
+        butil::atomic_thread_fence(butil::memory_order_seq_cst);
+        // Re-check _pending_signal with a real atomic load, which closes the 
Dekker
+        // pattern at the C++ memory model level (the check inside futex_wait 
is done
+        // by the kernel and is invisible to the compiler). It also saves a 
futex
+        // syscall that would return EAGAIN immediately.
+        if (_pending_signal.load(butil::memory_order_relaxed) == 
expected_state.val) {
+            futex_wait_private(&_pending_signal, expected_state.val, nullptr);
         }
+        _waiter_num.fetch_sub(1, butil::memory_order_relaxed);
     }
 
     // Wakeup suspended wait() and make them unwaitable ever. 
diff --git a/test/bthread_parking_lot_unittest.cpp 
b/test/bthread_parking_lot_unittest.cpp
new file mode 100644
index 00000000..d14902a9
--- /dev/null
+++ b/test/bthread_parking_lot_unittest.cpp
@@ -0,0 +1,473 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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 <limits.h>
+#include <unistd.h>
+#include <cstdlib>
+#include <thread>
+#include <vector>
+#include <gflags/gflags.h>
+#include <gtest/gtest.h>
+#include "butil/time.h"
+#include "bthread/parking_lot.h"
+
+namespace {
+
+using bthread::ParkingLot;
+
+const int64_t kTimeoutUs = 5 * 1000 * 1000;
+
+template <typename Predicate>
+bool wait_until(Predicate predicate) {
+    const int64_t deadline_us = butil::cpuwide_time_us() + kTimeoutUs;
+    int spins = 0;
+    while (!predicate()) {
+        if (butil::cpuwide_time_us() >= deadline_us) {
+            return false;
+        }
+        // Keep short handshakes responsive so the signaler can race with 
waiter
+        // registration instead of usually arriving after the waiter has slept.
+        if (++spins < 256) {
+            std::this_thread::yield();
+        } else {
+            usleep(50);
+        }
+    }
+    return true;
+}
+
+// ParkingLot blocks OS threads, so use native threads rather than bthreads.
+// All assertions are made before cleanup: rescue wakeups must not make a lost
+// wakeup test pass. The guard also cleans up on a fatal assertion/early 
return.
+class ParkingLotThreads {
+public:
+    explicit ParkingLotThreads(ParkingLot* lot)
+        : _lot(lot), _cancelled(false), _finished(0) {}
+
+    ~ParkingLotThreads() {
+        _cancelled.store(true, butil::memory_order_release);
+        const bool joined = wait_until([this] {
+            if (_finished.load(butil::memory_order_acquire) == 
_threads.size()) {
+                return true;
+            }
+            // Do not depend on signal() or stop() working to rescue a failure.
+            // Changing the value also rescues a thread not yet in futex_wait.
+            _lot->_pending_signal.fetch_add(2, butil::memory_order_release);
+            bthread::futex_wake_private(&_lot->_pending_signal, INT_MAX);
+            return false;
+        });
+        if (!joined) {
+            ADD_FAILURE() << "ParkingLot test threads could not be rescued";
+            // Never detach threads referencing stack objects or hang the 
suite.
+            std::abort();
+        }
+        for (auto& thread : _threads) {
+            thread.join();
+        }
+    }
+
+    template <typename Function>
+    void start(Function function) {
+        _threads.emplace_back([this, function] {
+            function();
+            _finished.fetch_add(1, butil::memory_order_release);
+        });
+    }
+
+    bool cancelled() const {
+        return _cancelled.load(butil::memory_order_acquire);
+    }
+
+    bool finished() const {
+        return _finished.load(butil::memory_order_acquire) == _threads.size();
+    }
+
+private:
+    ParkingLot* _lot;
+    butil::atomic<bool> _cancelled;
+    butil::atomic<size_t> _finished;
+    std::vector<std::thread> _threads;
+};
+
+class ParkingLotTest : public ::testing::TestWithParam<bool> {
+protected:
+    void SetUp() override {
+        bthread::FLAGS_parking_lot_no_signal_when_no_waiter = GetParam();
+    }
+
+    // Test builds use -fno-access-control. Inspect state and waiter 
bookkeeping
+    // without adding test-only hooks to the production synchronization path.
+    void check_wake_count(int num_waiters, int num_task, int expected_woken) {
+        ParkingLot lot;
+        butil::atomic<int> ready(0);
+        butil::atomic<int> returned(0);
+        ParkingLotThreads threads(&lot);
+        for (int i = 0; i < num_waiters; ++i) {
+            threads.start([&] {
+                ready.fetch_add(1, butil::memory_order_release);
+                while (!threads.cancelled()) {
+                    const auto state = lot.get_state();
+                    lot.wait(state);
+                    returned.fetch_add(1, butil::memory_order_release);
+                }
+            });
+        }
+        ASSERT_TRUE(wait_until([&] { return ready.load() == num_waiters; }));
+        if (GetParam()) {
+            ASSERT_TRUE(wait_until([&] {
+                return lot._waiter_num.load() == num_waiters;
+            }));
+        }
+
+        // Neither ready nor _waiter_num proves that the kernel has enqueued a
+        // waiter. Retry until the actual futex return value confirms the 
desired
+        // batch. This avoids assuming that a sleep is a scheduling barrier.
+        int nwoken = -1;
+        ASSERT_TRUE(wait_until([&] {
+            nwoken = lot.signal(num_task);
+            return nwoken == expected_woken || nwoken < 0 ||
+                   nwoken > expected_woken;
+        }));
+        ASSERT_EQ(expected_woken, nwoken);
+        ASSERT_TRUE(wait_until([&] { return returned.load() >= nwoken; }));
+    }
+
+private:
+    GFLAGS_NAMESPACE::FlagSaver _flag_saver;
+};
+
+TEST_P(ParkingLotTest, initial_state) {
+    ParkingLot::State state;
+    ASSERT_EQ(0, state.val);
+    ASSERT_FALSE(state.stopped());
+
+    ParkingLot lot;
+    ASSERT_EQ(0, lot.get_state().val);
+    ASSERT_FALSE(lot.get_state().stopped());
+    ASSERT_EQ(0, lot._waiter_num.load());
+    ASSERT_EQ(GetParam(), lot._no_signal_when_no_waiter);
+}
+
+TEST_P(ParkingLotTest, configuration_is_captured_at_construction) {
+    ParkingLot first;
+    bthread::FLAGS_parking_lot_no_signal_when_no_waiter = !GetParam();
+    ParkingLot second;
+    ASSERT_EQ(GetParam(), first._no_signal_when_no_waiter);
+    ASSERT_EQ(!GetParam(), second._no_signal_when_no_waiter);
+    ASSERT_EQ(0, first.signal(1));
+    ASSERT_EQ(0, second.signal(1));
+    ASSERT_EQ(2, first.get_state().val);
+    ASSERT_EQ(2, second.get_state().val);
+}
+
+TEST_P(ParkingLotTest, signal_without_waiters_updates_state) {
+    ParkingLot lot;
+    const auto initial = lot.get_state();
+    ASSERT_EQ(0, lot.signal(1));
+    const auto first = lot.get_state();
+    ASSERT_EQ(2, first.val);
+    ASSERT_EQ(0, lot.signal(3));
+    ASSERT_EQ(8, lot.get_state().val);
+    ASSERT_EQ(0, lot.signal(0));
+    ASSERT_EQ(8, lot.get_state().val);
+    ASSERT_FALSE(lot.get_state().stopped());
+    // Snapshots are values, not references to the live signal counter.
+    ASSERT_EQ(0, initial.val);
+    ASSERT_EQ(2, first.val);
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, stale_state_does_not_wait_or_consume_signals) {
+    ParkingLot lot;
+    const auto initial = lot.get_state();
+    lot.signal(1);
+    const auto first = lot.get_state();
+    lot.signal(2);
+    ParkingLotThreads threads(&lot);
+    threads.start([&] {
+        for (int i = 0; i < 100 && !threads.cancelled(); ++i) {
+            lot.wait(initial);
+            lot.wait(first);
+        }
+    });
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_EQ(6, lot.get_state().val);
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, current_state_waits_even_after_previous_signals) {
+    ParkingLot lot;
+    lot.signal(3);
+    const auto current = lot.get_state();
+    butil::atomic<bool> ready(false);
+    butil::atomic<int> returned(0);
+    ParkingLotThreads threads(&lot);
+    threads.start([&] {
+        ready.store(true, butil::memory_order_release);
+        // Allow spurious wakeups without mistaking them for a new signal.
+        while (!threads.cancelled() && lot.get_state().val == current.val) {
+            lot.wait(current);
+        }
+        returned.store(1, butil::memory_order_release);
+    });
+    ASSERT_TRUE(wait_until([&] { return ready.load(); }));
+    if (GetParam()) {
+        ASSERT_TRUE(wait_until([&] { return lot._waiter_num.load() == 1; }));
+    }
+    usleep(10 * 1000);
+    ASSERT_EQ(0, returned.load());
+    const int nwoken = lot.signal(1);
+    ASSERT_GE(nwoken, 0);
+    ASSERT_LE(nwoken, 1);
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_EQ(1, returned.load());
+    ASSERT_EQ(8, lot.get_state().val);
+    ASSERT_EQ(0, lot._waiter_num.load());
+    ASSERT_EQ(0, lot.signal(1));
+}
+
+TEST_P(ParkingLotTest, signal_wakes_one_waiter) {
+    check_wake_count(1, 1, 1);
+}
+
+TEST_P(ParkingLotTest, signal_wakes_a_limited_batch) {
+    check_wake_count(4, 2, 2);
+}
+
+TEST_P(ParkingLotTest, signal_is_limited_by_available_waiters) {
+    check_wake_count(4, 10, 4);
+}
+
+TEST_P(ParkingLotTest, stop_preserves_signal_count_and_is_idempotent) {
+    ParkingLot lot;
+    lot.signal(3);
+    const auto before_stop = lot.get_state();
+    lot.stop();
+    const auto stopped = lot.get_state();
+    ASSERT_TRUE(stopped.stopped());
+    ASSERT_EQ(7, stopped.val);
+    lot.stop();
+    ASSERT_EQ(stopped.val, lot.get_state().val);
+    ASSERT_EQ(0, lot.signal(2));
+    ASSERT_EQ(11, lot.get_state().val);
+    ASSERT_TRUE(lot.get_state().stopped());
+    ASSERT_FALSE(before_stop.stopped());
+    ASSERT_EQ(6, before_stop.val);
+    ASSERT_EQ(7, stopped.val);
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, stop_before_wait_invalidates_old_snapshots) {
+    ParkingLot lot;
+    const auto initial = lot.get_state();
+    lot.signal(2);
+    const auto before_stop = lot.get_state();
+    lot.stop();
+    ParkingLotThreads threads(&lot);
+    threads.start([&] {
+        // Callers must check State::stopped() before waiting. Only snapshots
+        // taken before stop(), not a fresh stopped snapshot, are waitable 
here.
+        lot.wait(initial);
+        lot.wait(before_stop);
+    });
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_TRUE(lot.get_state().stopped());
+    ASSERT_EQ(5, lot.get_state().val);
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, stop_wakes_all_waiters) {
+    const int num_waiters = 8;
+    ParkingLot lot;
+    const auto initial = lot.get_state();
+    butil::atomic<int> ready(0);
+    ParkingLotThreads threads(&lot);
+    for (int i = 0; i < num_waiters; ++i) {
+        threads.start([&] {
+            ready.fetch_add(1, butil::memory_order_release);
+            while (!threads.cancelled() && !lot.get_state().stopped()) {
+                lot.wait(initial);
+            }
+        });
+    }
+    ASSERT_TRUE(wait_until([&] { return ready.load() == num_waiters; }));
+    if (GetParam()) {
+        ASSERT_TRUE(wait_until([&] {
+            return lot._waiter_num.load() == num_waiters;
+        }));
+    }
+    lot.stop();
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_TRUE(lot.get_state().stopped());
+    ASSERT_EQ(1, lot.get_state().val);
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, concurrent_signals_are_not_lost) {
+    const int num_signalers = 4;
+    const int iterations = 1000;
+    ParkingLot lot;
+    butil::atomic<int> bad_returns(0);
+    ParkingLotThreads threads(&lot);
+    for (int i = 1; i <= num_signalers; ++i) {
+        threads.start([&, i] {
+            for (int j = 0; j < iterations && !threads.cancelled(); ++j) {
+                if (lot.signal(i) != 0) {
+                    bad_returns.fetch_add(1);
+                }
+            }
+        });
+    }
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_EQ(0, bad_returns.load());
+    ASSERT_EQ(iterations * num_signalers * (num_signalers + 1),
+              lot.get_state().val);
+    ASSERT_FALSE(lot.get_state().stopped());
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, get_state_acquires_published_data) {
+    ParkingLot lot;
+    int payload = 0;
+    ParkingLotThreads threads(&lot);
+    threads.start([&] {
+        payload = 42;
+        lot.signal(1);
+    });
+    // Observe the release in signal() through get_state(), not thread.join()
+    // or the helper's completion counter, before reading the non-atomic data.
+    ASSERT_TRUE(wait_until([&] { return lot.get_state().val != 0; }));
+    ASSERT_EQ(42, payload);
+}
+
+TEST_P(ParkingLotTest, signal_counter_wraparound_preserves_stop_bit) {
+    ParkingLot lot;
+    // Atomic signed fetch_add wraps without signed-overflow UB. Keep num_task
+    // small enough that the separate signed left shift in signal() is valid.
+    ASSERT_EQ(0, lot.signal(INT_MAX / 2));
+    ASSERT_EQ(INT_MAX - 1, lot.get_state().val);
+    ASSERT_EQ(0, lot.signal(1));
+    ASSERT_EQ(INT_MIN, lot.get_state().val);
+    ASSERT_FALSE(lot.get_state().stopped());
+
+    lot.stop();
+    ASSERT_EQ(INT_MIN + 1, lot.get_state().val);
+    ASSERT_EQ(0, lot.signal(INT_MAX / 2));
+    ASSERT_EQ(-1, lot.get_state().val);
+    ASSERT_EQ(0, lot.signal(1));
+    ASSERT_EQ(1, lot.get_state().val);
+    ASSERT_TRUE(lot.get_state().stopped());
+}
+
+TEST_P(ParkingLotTest, stop_races_with_signals_and_wait) {
+    const int iterations = 1000;
+    ParkingLot lot;
+    butil::atomic<bool> start(false);
+    ParkingLotThreads threads(&lot);
+    threads.start([&] {
+        while (!threads.cancelled()) {
+            const auto state = lot.get_state();
+            if (state.stopped()) {
+                return;
+            }
+            lot.wait(state);
+        }
+    });
+    threads.start([&] {
+        while (!start.load(butil::memory_order_acquire)) {
+            if (threads.cancelled()) {
+                return;
+            }
+            std::this_thread::yield();
+        }
+        for (int i = 0; i < iterations && !threads.cancelled(); ++i) {
+            lot.signal(1);
+        }
+    });
+    threads.start([&] {
+        while (!start.load(butil::memory_order_acquire)) {
+            if (threads.cancelled()) {
+                return;
+            }
+            std::this_thread::yield();
+        }
+        for (int i = 0; i < iterations && !threads.cancelled(); ++i) {
+            lot.stop();
+        }
+    });
+    start.store(true, butil::memory_order_release);
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_EQ(iterations * 2 + 1, lot.get_state().val);
+    ASSERT_TRUE(lot.get_state().stopped());
+    ASSERT_EQ(0, lot._waiter_num.load());
+}
+
+TEST_P(ParkingLotTest, racing_signal_and_wait_do_not_lose_wakeups) {
+    const int iterations = 2000;
+    ParkingLot lot;
+    butil::atomic<int> start(0);
+    butil::atomic<int> ready(0);
+    butil::atomic<int> completed(0);
+    ParkingLotThreads threads(&lot);
+    threads.start([&] {
+        for (int round = 1; round <= iterations; ++round) {
+            while (start.load(butil::memory_order_acquire) != round) {
+                if (threads.cancelled()) {
+                    return;
+                }
+                std::this_thread::yield();
+            }
+            const auto state = lot.get_state();
+            ready.store(round, butil::memory_order_release);
+            if (round % 3 == 0) {
+                std::this_thread::yield();
+            }
+            lot.wait(state);
+            completed.store(round, butil::memory_order_release);
+        }
+    });
+
+    for (int round = 1; round <= iterations; ++round) {
+        SCOPED_TRACE(round);
+        start.store(round, butil::memory_order_release);
+        ASSERT_TRUE(wait_until([&] {
+            return ready.load(butil::memory_order_acquire) == round;
+        }));
+        if (round % 3 == 1) {
+            std::this_thread::yield();
+        }
+        // The handshake precedes BOTH the waiter registration and signal().
+        // Do not wait for _waiter_num here: that would remove the Dekker race.
+        // Exactly one signal per round; no later signal may rescue this round.
+        const int nwoken = lot.signal(1);
+        ASSERT_GE(nwoken, 0);
+        ASSERT_LE(nwoken, 1);
+        ASSERT_TRUE(wait_until([&] {
+            return completed.load(butil::memory_order_acquire) == round;
+        })) << "Possible lost wakeup";
+        ASSERT_EQ(0, lot._waiter_num.load());
+    }
+    ASSERT_TRUE(wait_until([&] { return threads.finished(); }));
+    ASSERT_EQ(iterations * 2, lot.get_state().val);
+    ASSERT_EQ(0, lot.signal(1));
+    // This is a real-implementation stress regression, not a deterministic
+    // proof of weak-memory ordering. Removing a fence need not fail on x86.
+}
+
+INSTANTIATE_TEST_SUITE_P(WaiterCheckModes, ParkingLotTest, ::testing::Bool());
+
+}  // namespace


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to