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

wwbmmm 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 8d4aa518 Fix macOS futex wake accounting with per-waiter notifications 
(#3637)
8d4aa518 is described below

commit 8d4aa518352d7dc51e5dbf9f37c12dd1482e3e2c
Author: Bright Chen <[email protected]>
AuthorDate: Thu Oct 22 21:21:24 2026 +0800

    Fix macOS futex wake accounting with per-waiter notifications (#3637)
    
    * Fix macOS futex wake accounting with per-waiter notifications
    
    * Fix ut
    
    * Guard macOS futex waiter cleanup and fix timeout test diagnostic
---
 src/bthread/sys_futex.cpp       |  76 ++++++++++++++++++++----------
 test/bthread_futex_unittest.cpp | 101 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 152 insertions(+), 25 deletions(-)

diff --git a/src/bthread/sys_futex.cpp b/src/bthread/sys_futex.cpp
index 010ddcad..b6ebdd60 100644
--- a/src/bthread/sys_futex.cpp
+++ b/src/bthread/sys_futex.cpp
@@ -22,6 +22,7 @@
 #include "bthread/sys_futex.h"
 #include "butil/scoped_lock.h"
 #include "butil/atomicops.h"
+#include "butil/containers/linked_list.h"
 #include <pthread.h>
 #include <unordered_map>
 
@@ -29,22 +30,35 @@
 
 namespace bthread {
 
-class SimuFutex {
-public:
-    SimuFutex() : counts(0)
-                , ref(0) {
-        pthread_mutex_init(&lock, nullptr);
+struct SimuFutexWaiter : public butil::LinkNode<SimuFutexWaiter> {
+    SimuFutexWaiter() : signaled(false) {
         pthread_cond_init(&cond, nullptr);
     }
+    ~SimuFutexWaiter() {
+        // The futex mutex is still held when the stack waiter is destroyed.
+        // Notified waiters were detached by futex_wake_private. Remove only
+        // waiters that leave on timeout or error.
+        if (!signaled) {
+            RemoveFromList();
+        }
+        pthread_cond_destroy(&cond);
+    }
+
+    pthread_cond_t cond;
+    bool signaled;
+};
+
+struct SimuFutex {
+    SimuFutex() : ref(0) {
+        pthread_mutex_init(&lock, nullptr);
+    }
     ~SimuFutex() {
         pthread_mutex_destroy(&lock);
-        pthread_cond_destroy(&cond);
     }
 
-public:
     pthread_mutex_t lock;
-    pthread_cond_t cond;
-    int32_t counts;
+    // Only waiters that have not been notified are in this queue.
+    butil::LinkedList<SimuFutexWaiter> waiters;
     int32_t ref;
 };
 
@@ -70,20 +84,28 @@ int futex_wait_private(void* addr1, int expected, const 
timespec* timeout) {
     {
         std::unique_lock<pthread_mutex_t> mu1(simu_futex.lock);
         if (static_cast<butil::atomic<int>*>(addr1)->load() == expected) {
-            ++simu_futex.counts;
+            SimuFutexWaiter waiter;
+            simu_futex.waiters.Append(&waiter);
+            timespec timeout_abs = {};
             if (timeout) {
-                timespec timeout_abs = butil::timespec_from_now(*timeout);
-                if ((rc = pthread_cond_timedwait(&simu_futex.cond, 
&simu_futex.lock, &timeout_abs)) != 0) {
-                    errno = rc;
-                    rc = -1;
-                }
-            } else {
-                if ((rc = pthread_cond_wait(&simu_futex.cond, 
&simu_futex.lock)) != 0) {
-                    errno = rc;
-                    rc = -1;
+                timeout_abs = butil::timespec_from_now(*timeout);
+            }
+            do {
+                if (timeout) {
+                    rc = pthread_cond_timedwait(
+                        &waiter.cond, &simu_futex.lock, &timeout_abs);
+                } else {
+                    rc = pthread_cond_wait(&waiter.cond, &simu_futex.lock);
                 }
+            } while (rc == 0 && !waiter.signaled);
+            // A wake may win the mutex while a timed-out waiter is reacquiring
+            // it. Honor that notification so it cannot be counted and then 
lost.
+            if (waiter.signaled) {
+                rc = 0;
+            } else if (rc != 0) {
+                errno = rc;
+                rc = -1;
             }
-            --simu_futex.counts;
         } else {
             errno = EAGAIN;
             rc = -1;
@@ -117,14 +139,18 @@ int futex_wake_private(void* addr1, int nwake) {
     int rc = 0;
     {
         std::unique_lock<pthread_mutex_t> mu1(simu_futex.lock);
-        nwake = (nwake < simu_futex.counts)? nwake: simu_futex.counts;
-        for (int i = 0; i < nwake; ++i) {
-            if ((rc = pthread_cond_signal(&simu_futex.cond)) != 0) {
+        while (nwakedup < nwake && !simu_futex.waiters.empty()) {
+            SimuFutexWaiter* waiter = simu_futex.waiters.head()->value();
+            if ((rc = pthread_cond_signal(&waiter->cond)) != 0) {
                 errno = rc;
                 break;
-            } else {
-                ++nwakedup;
             }
+            // Commit the notification to this waiter before releasing the
+            // mutex. Later wakes cannot count it again while it is 
rescheduled,
+            // and another waiter's timeout cannot consume its notification.
+            waiter->signaled = true;
+            waiter->RemoveFromList();
+            ++nwakedup;
         }
     }
 
diff --git a/test/bthread_futex_unittest.cpp b/test/bthread_futex_unittest.cpp
index ce32356e..d08982b6 100644
--- a/test/bthread_futex_unittest.cpp
+++ b/test/bthread_futex_unittest.cpp
@@ -19,6 +19,7 @@
 #include <unistd.h>
 #include <stdio.h>
 #include <signal.h>
+#include <pthread.h>
 #include <gtest/gtest.h>
 #include <vector>
 #include "butil/time.h"
@@ -115,6 +116,106 @@ TEST(FutexTest, futex_wake_before_wait) {
     ASSERT_EQ(ETIMEDOUT, errno);
 }
 
+const int kWakeCountIterations = 256;
+const int64_t kWakeCountTimeoutUs = 5 * 1000 * 1000L;
+const useconds_t kWakeCountRetryIntervalUs = 50;
+
+struct WakeCountWaiterArg {
+    butil::atomic<int>* word;
+    butil::atomic<bool>* finished;
+    int64_t deadline_us;
+    int rc;
+    int error;
+};
+
+void* wake_count_waiter(void* void_arg) {
+    WakeCountWaiterArg* arg = static_cast<WakeCountWaiterArg*>(void_arg);
+    const int64_t remaining_us = arg->deadline_us - butil::cpuwide_time_us();
+    const timespec timeout = butil::microseconds_to_timespec(
+        remaining_us > 0 ? remaining_us : 0);
+    arg->rc = bthread::futex_wait_private(arg->word, 0, &timeout);
+    arg->error = errno;
+    arg->finished->store(true, butil::memory_order_release);
+    return nullptr;
+}
+
+// A successful wake confirms that the waiter was registered. The waiter never
+// waits again, so every subsequent wake must return zero, even before it runs.
+// Repeat to exercise both orders of reacquiring the simulated futex's mutex.
+void check_wake_count_after_notification(bool expire_another_waiter) {
+    // Share one deadline across all iterations and the waiter itself, so slow
+    // scheduling cannot renew the timeout on every iteration.
+    const int64_t deadline_us = butil::cpuwide_time_us() + kWakeCountTimeoutUs;
+    for (int i = 0; i < kWakeCountIterations; ++i) {
+        ASSERT_LT(butil::cpuwide_time_us(), deadline_us)
+            << "Timed out after " << i << " iterations";
+        butil::atomic<int> word(0);
+        butil::atomic<bool> finished(false);
+        WakeCountWaiterArg arg = { &word, &finished, deadline_us, -1, 0 };
+        pthread_t waiter;
+        ASSERT_EQ(0, pthread_create(&waiter, nullptr, wake_count_waiter, 
&arg));
+
+        int first_wake = 0;
+        // A readiness flag published before futex_wait does not prove that the
+        // waiter is queued. Use the wake result, with a pause between retries.
+        while (butil::cpuwide_time_us() < deadline_us) {
+            first_wake = bthread::futex_wake_private(&word, 1);
+            if (first_wake != 0 || finished.load(butil::memory_order_acquire)) 
{
+                break;
+            }
+            usleep(kWakeCountRetryIntervalUs);
+        }
+
+        int expired_rc = 0;
+        int expired_errno = 0;
+        int second_wake = 0;
+        if (first_wake == 1) {
+            if (expire_another_waiter) {
+                const timespec timeout = { 0, 0 };
+                expired_rc = bthread::futex_wait_private(&word, 0, &timeout);
+                expired_errno = errno;
+            }
+            second_wake = bthread::futex_wake_private(&word, 1);
+        }
+        // The waiter has a timeout even if no wake succeeds. Join before
+        // checking results so a failed assertion cannot leave it referring to
+        // this iteration's stack objects.
+        ASSERT_EQ(0, pthread_join(waiter, nullptr));
+        ASSERT_EQ(1, first_wake)
+            << "No waiter was notified before the deadline: iteration=" << i
+            << " waiter_rc=" << arg.rc << " errno=" << arg.error;
+        ASSERT_EQ(0, arg.rc) << "errno=" << arg.error;
+        if (expire_another_waiter) {
+            ASSERT_EQ(-1, expired_rc);
+            ASSERT_EQ(ETIMEDOUT, expired_errno);
+        }
+        ASSERT_EQ(0, second_wake) << "iteration=" << i;
+    }
+}
+
+TEST(FutexTest, notified_waiter_is_not_counted_twice) {
+    check_wake_count_after_notification(false);
+}
+
+TEST(FutexTest, timeout_does_not_consume_another_waiters_notification) {
+    check_wake_count_after_notification(true);
+}
+
+TEST(FutexTest, timed_out_waiter_is_removed) {
+    butil::atomic<int> word(0);
+    const timespec timeout = { 0, 0 };
+    ASSERT_EQ(-1, bthread::futex_wait_private(&word, 0, &timeout));
+    ASSERT_EQ(ETIMEDOUT, errno);
+    ASSERT_EQ(0, bthread::futex_wake_private(&word, INT_MAX));
+}
+
+TEST(FutexTest, mismatched_value_does_not_queue_a_waiter) {
+    butil::atomic<int> word(1);
+    ASSERT_EQ(-1, bthread::futex_wait_private(&word, 0, nullptr));
+    ASSERT_EQ(EAGAIN, errno);
+    ASSERT_EQ(0, bthread::futex_wake_private(&word, INT_MAX));
+}
+
 struct DummyWaiterArg {
     butil::atomic<int>* lock;
     butil::atomic<int>* registered;


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

Reply via email to