| // 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 |