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 aff4d82e8 Fix MPSC queue publication races in butil, bthread, and 
Socket (#3671)
aff4d82e8 is described below

commit aff4d82e82f47a0a35be532d9add6a06828ea84a
Author: nkysg <[email protected]>
AuthorDate: Sun Nov 8 12:29:39 2026 +0800

    Fix MPSC queue publication races in butil, bthread, and Socket (#3671)
    
    * Fix MPSCQueue publication races
    
    * Fix execution and socket queue publication races
---
 src/brpc/socket.cpp               | 46 ++++++++++++++++-------------
 src/bthread/execution_queue.cpp   | 28 +++++++++---------
 src/bthread/execution_queue_inl.h | 15 +++++-----
 src/butil/containers/mpsc_queue.h | 33 +++++++++++----------
 test/mpsc_queue_unittest.cc       | 61 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 127 insertions(+), 56 deletions(-)

diff --git a/src/brpc/socket.cpp b/src/brpc/socket.cpp
index c4411c960..2985a35b6 100644
--- a/src/brpc/socket.cpp
+++ b/src/brpc/socket.cpp
@@ -311,7 +311,7 @@ struct BAIDU_CACHELINE_ALIGNMENT Socket::WriteRequest {
     static WriteRequest* const UNCONNECTED;
 
     butil::IOBuf data;
-    WriteRequest* next;
+    butil::atomic<WriteRequest*> next;
     bthread_id_t id_wait;
 
     void clear_and_set_control_bits(bool notify_on_success,
@@ -533,8 +533,9 @@ void Socket::ReturnFailedWriteRequest(Socket::WriteRequest* 
p, int error_code,
 Socket::WriteRequest* Socket::ReleaseWriteRequestsExceptLast(
     Socket::WriteRequest* req, int error_code, const std::string& error_text) {
     WriteRequest* p = req;
-    while (p->next != nullptr) {
-        WriteRequest* const saved_next = p->next;
+    while (p->next.load(butil::memory_order_relaxed) != nullptr) {
+        WriteRequest* const saved_next =
+            p->next.load(butil::memory_order_relaxed);
         ReturnFailedWriteRequest(p, error_code, error_text);
         p = saved_next;
     }
@@ -1195,7 +1196,7 @@ int Socket::Status(SocketId id, int32_t* nref) {
 bool Socket::IsWriteComplete(Socket::WriteRequest* old_head,
                              bool singular_node,
                              Socket::WriteRequest** new_tail) {
-    CHECK(nullptr == old_head->next);
+    CHECK(nullptr == old_head->next.load(butil::memory_order_relaxed));
     // Try to set _write_head to nullptr to mark that the write is done.
     WriteRequest* new_head = old_head;
     WriteRequest* desired = nullptr;
@@ -1222,23 +1223,25 @@ bool Socket::IsWriteComplete(Socket::WriteRequest* 
old_head,
     WriteRequest* tail = nullptr;
     WriteRequest* p = new_head;
     do {
-        while (p->next == WriteRequest::UNCONNECTED) {
+        WriteRequest* saved_next;
+        while ((saved_next = p->next.load(butil::memory_order_acquire)) ==
+               WriteRequest::UNCONNECTED) {
             // TODO(gejun): elaborate this
             sched_yield();
         }
-        WriteRequest* const saved_next = p->next;
-        p->next = tail;
+        p->next.store(tail, butil::memory_order_relaxed);
         tail = p;
         p = saved_next;
         CHECK(p != nullptr);
     } while (p != old_head);
 
     // Link old list with new list.
-    old_head->next = tail;
+    old_head->next.store(tail, butil::memory_order_relaxed);
     // Call Setup() from oldest to newest, notice that the calling sequence
     // matters for protocols using pipelined_count, this is why we don't
     // call Setup in above loop which is from newest to oldest.
-    for (WriteRequest* q = tail; q; q = q->next) {
+    for (WriteRequest* q = tail; q;
+         q = q->next.load(butil::memory_order_relaxed)) {
         q->Setup(this);
     }
     if (new_tail) {
@@ -1669,7 +1672,7 @@ int Socket::Write(butil::IOBuf* data, const WriteOptions* 
options_in) {
     req->data.swap(*data);
     // Set `req->next' to UNCONNECTED so that the KeepWrite thread will
     // wait until it points to a valid WriteRequest or nullptr.
-    req->next = WriteRequest::UNCONNECTED;
+    req->next.store(WriteRequest::UNCONNECTED, butil::memory_order_relaxed);
     req->id_wait = opt.id_wait;
     req->clear_and_set_control_bits(opt.notify_on_success, opt.shutdown_write);
     req->set_pipelined_count_and_user_message(
@@ -1706,7 +1709,7 @@ int Socket::Write(SocketMessagePtr<>& msg, const 
WriteOptions* options_in) {
 
     // Set `req->next' to UNCONNECTED so that the KeepWrite thread will
     // wait until it points to a valid WriteRequest or nullptr.
-    req->next = WriteRequest::UNCONNECTED;
+    req->next.store(WriteRequest::UNCONNECTED, butil::memory_order_relaxed);
     req->id_wait = opt.id_wait;
     req->clear_and_set_control_bits(opt.notify_on_success, opt.shutdown_write);
     req->set_pipelined_count_and_user_message(
@@ -1724,7 +1727,7 @@ int Socket::StartWrite(WriteRequest* req, const 
WriteOptions& opt) {
         // lock-free, but the duration is so short(1~2 instructions,
         // depending on compiler) that the spin rarely occurs in practice
         // (I've not seen any spin in highly contended tests).
-        req->next = prev_head;
+        req->next.store(prev_head, butil::memory_order_release);
         return 0;
     }
 
@@ -1737,7 +1740,7 @@ int Socket::StartWrite(WriteRequest* req, const 
WriteOptions& opt) {
     int ret = 0;
 
     // We've got the right to write.
-    req->next = nullptr;
+    req->next.store(nullptr, butil::memory_order_relaxed);
 
     // Fast fail when write has been shutdown.
     if (_is_write_shutdown) {
@@ -1827,10 +1830,11 @@ void* Socket::KeepWrite(void* void_arg) {
     do {
         // req was written, skip it.
         bool need_shutdown = false;
-        if (req->next != nullptr && req->data.empty()) {
+        if (req->next.load(butil::memory_order_relaxed) != nullptr &&
+            req->data.empty()) {
             WriteRequest* const saved_req = req;
             need_shutdown = req->need_shutdown_write();
-            req = req->next;
+            req = req->next.load(butil::memory_order_relaxed);
             s->ReturnSuccessfulWriteRequest(saved_req);
         }
         if (need_shutdown) {
@@ -1851,10 +1855,11 @@ void* Socket::KeepWrite(void* void_arg) {
             s->AddOutputBytes(nw);
         }
         // Release WriteRequest until non-empty data, last request or shutdown 
write.
-        while (req->next != nullptr && req->data.empty()) {
+        while (req->next.load(butil::memory_order_relaxed) != nullptr &&
+               req->data.empty()) {
             WriteRequest* const saved_req = req;
             need_shutdown = req->need_shutdown_write();
-            req = req->next;
+            req = req->next.load(butil::memory_order_relaxed);
             s->ReturnSuccessfulWriteRequest(saved_req);
             if (need_shutdown) {
                 break;
@@ -1884,8 +1889,9 @@ void* Socket::KeepWrite(void* void_arg) {
             }
         }
         if (nullptr == cur_tail) {
-            for (cur_tail = req; cur_tail->next != nullptr;
-                 cur_tail = cur_tail->next);
+            for (cur_tail = req;
+                 cur_tail->next.load(butil::memory_order_relaxed) != nullptr;
+                 cur_tail = cur_tail->next.load(butil::memory_order_relaxed));
         }
         // Return when there's no more WriteRequests and req is completely
         // written.
@@ -1906,7 +1912,7 @@ ssize_t Socket::DoWrite(WriteRequest* req) {
     butil::IOBuf* data_list[DATA_LIST_MAX];
     size_t ndata = 0;
     for (WriteRequest* p = req; p != nullptr && ndata < DATA_LIST_MAX;
-         p = p->next) {
+         p = p->next.load(butil::memory_order_relaxed)) {
         data_list[ndata++] = &p->data;
         if (p->need_shutdown_write()) {
             // Write WriteRequest until shutdown write.
diff --git a/src/bthread/execution_queue.cpp b/src/bthread/execution_queue.cpp
index de4069b12..2d48deb50 100644
--- a/src/bthread/execution_queue.cpp
+++ b/src/bthread/execution_queue.cpp
@@ -66,7 +66,7 @@ inline ExecutionQueueVars* get_execq_vars() {
 }
 
 void ExecutionQueueBase::start_execute(TaskNode* node) {
-    node->next = TaskNode::UNCONNECTED;
+    node->next.store(TaskNode::UNCONNECTED, butil::memory_order_relaxed);
     node->status = TaskNode::UNEXECUTED;
     node->iterated = false;
     if (node->high_priority) {
@@ -79,12 +79,12 @@ void ExecutionQueueBase::start_execute(TaskNode* node) {
     }
     TaskNode* const prev_head = _head.exchange(node, 
butil::memory_order_release);
     if (prev_head != nullptr) {
-        node->next = prev_head;
+        node->next.store(prev_head, butil::memory_order_release);
         return;
     }
     // Get the right to execute the task, start a bthread to avoid deadlock
     // or stack overflow
-    node->next = nullptr;
+    node->next.store(nullptr, butil::memory_order_relaxed);
     node->q = this;
 
     ExecutionQueueVars* const vars = get_execq_vars();
@@ -149,9 +149,9 @@ void* ExecutionQueueBase::_execute_tasks(void* arg) {
     bool destroy_queue = false;
     for (;;) {
         if (head->iterated) {
-            CHECK(head->next != nullptr);
+            CHECK(head->next.load(butil::memory_order_relaxed) != nullptr);
             TaskNode* saved_head = head;
-            head = head->next;
+            head = head->next.load(butil::memory_order_relaxed);
             m->return_task_node(saved_head);
         }
         int rc = 0;
@@ -172,14 +172,16 @@ void* ExecutionQueueBase::_execute_tasks(void* arg) {
             destroy_queue = true;
         }
         // Release TaskNode until uniterated task or last task
-        while (head->next != nullptr && head->iterated) {
+        while (head->next.load(butil::memory_order_relaxed) != nullptr &&
+               head->iterated) {
             TaskNode* saved_head = head;
-            head = head->next;
+            head = head->next.load(butil::memory_order_relaxed);
             m->return_task_node(saved_head);
         }
         if (cur_tail == nullptr) {
-            for (cur_tail = head; cur_tail->next != nullptr; 
-                    cur_tail = cur_tail->next) {}
+            for (cur_tail = head;
+                 cur_tail->next.load(butil::memory_order_relaxed) != nullptr;
+                 cur_tail = cur_tail->next.load(butil::memory_order_relaxed)) 
{}
         }
         // break when no more tasks and head has been executed
         if (!m->_more_tasks(cur_tail, &cur_tail, !head->iterated)) {
@@ -301,7 +303,7 @@ int ExecutionQueueBase::stop() {
 
 int ExecutionQueueBase::_execute(TaskNode* head, bool high_priority, int* 
niterated) {
     if (head != nullptr && head->stop_task) {
-        CHECK(head->next == nullptr);
+        CHECK(head->next.load(butil::memory_order_relaxed) == nullptr);
         head->iterated = true;
         head->status = TaskNode::EXECUTED;
         TaskIteratorBase iter(nullptr, this, true, false);
@@ -431,7 +433,7 @@ void TaskIteratorBase::operator++() {
         return;
     }
     if (_cur_node->iterated) {
-        _cur_node = _cur_node->next;
+        _cur_node = _cur_node->next.load(butil::memory_order_relaxed);
     }
     if (should_break_for_high_priority_tasks()) {
         return;
@@ -447,7 +449,7 @@ void TaskIteratorBase::operator++() {
             _num_iterated += !_cur_node->iterated;
             _cur_node->iterated = true;
         }
-        _cur_node = _cur_node->next;
+        _cur_node = _cur_node->next.load(butil::memory_order_relaxed);
     }
     return;
 }
@@ -463,7 +465,7 @@ TaskIteratorBase::~TaskIteratorBase() {
         if (_head->iterated && _head->high_priority == _high_priority) {
             _head->set_executed();
         }
-        _head = _head->next;
+        _head = _head->next.load(butil::memory_order_relaxed);
     }
     if (_should_break && _cur_node != nullptr 
             && _cur_node->high_priority == _high_priority && 
_cur_node->iterated) {
diff --git a/src/bthread/execution_queue_inl.h 
b/src/bthread/execution_queue_inl.h
index 2a85d27be..23c753311 100644
--- a/src/bthread/execution_queue_inl.h
+++ b/src/bthread/execution_queue_inl.h
@@ -57,7 +57,7 @@ struct BAIDU_CACHELINE_ALIGNMENT TaskNode {
         , iterated(false) 
         , high_priority(false)
         , in_place(false) 
-        , next(UNCONNECTED)
+        , next(nullptr)
         , q(nullptr)
     {}
     ~TaskNode() {}
@@ -91,7 +91,7 @@ struct BAIDU_CACHELINE_ALIGNMENT TaskNode {
     bool iterated;
     bool high_priority;
     bool in_place;
-    TaskNode* next;
+    butil::atomic<TaskNode*> next;
     ExecutionQueueBase* q;
     union {
         char static_task_mem[56];  // Make sizeof TaskNode exactly 128 bytes
@@ -486,7 +486,7 @@ inline bool ExecutionQueueBase::_more_tasks(
         TaskNode* old_head, TaskNode** new_tail, 
         bool has_uniterated) {
 
-    CHECK(old_head->next == nullptr);
+    CHECK(old_head->next.load(butil::memory_order_relaxed) == nullptr);
     // Try to set _head to nullptr to mark that the execute is done.
     TaskNode* new_head = old_head;
     TaskNode* desired = nullptr;
@@ -512,19 +512,20 @@ inline bool ExecutionQueueBase::_more_tasks(
     }
     TaskNode* p = new_head;
     do {
-        while (p->next == TaskNode::UNCONNECTED) {
+        TaskNode* saved_next;
+        while ((saved_next = p->next.load(butil::memory_order_acquire)) ==
+               TaskNode::UNCONNECTED) {
             // TODO(gejun): elaborate this
             sched_yield();
         }
-        TaskNode* const saved_next = p->next;
-        p->next = tail;
+        p->next.store(tail, butil::memory_order_relaxed);
         tail = p;
         p = saved_next;
         CHECK(p != nullptr);
     } while (p != old_head);
 
     // Link old list with new list.
-    old_head->next = tail;
+    old_head->next.store(tail, butil::memory_order_relaxed);
     return true;
 }
 
diff --git a/src/butil/containers/mpsc_queue.h 
b/src/butil/containers/mpsc_queue.h
index ff5aee0b2..87fd7ac22 100644
--- a/src/butil/containers/mpsc_queue.h
+++ b/src/butil/containers/mpsc_queue.h
@@ -22,6 +22,7 @@
 #ifndef BUTIL_MPSC_QUEUE_H
 #define BUTIL_MPSC_QUEUE_H
 
+#include <new>
 #include "butil/object_pool.h"
 #include "butil/type_traits.h"
 #include "butil/memory/manual_constructor.h"
@@ -32,7 +33,7 @@ template <typename T>
 struct BAIDU_CACHELINE_ALIGNMENT MPSCQueueNode {
     static MPSCQueueNode* const UNCONNECTED;
 
-    MPSCQueueNode* next{nullptr};
+    atomic<MPSCQueueNode*> next{nullptr};
     ManualConstructor<T> data_mem;
 };
 
@@ -107,8 +108,8 @@ void MPSCQueue<T, Alloc>::Enqueue(T&& data) {
 template <typename T, typename Alloc>
 template <typename U>
 void MPSCQueue<T, Alloc>::EnqueueInternal(U&& data) {
-    auto node = (MPSCQueueNode<T>*)_alloc.Alloc();
-    node->next = MPSCQueueNode<T>::UNCONNECTED;
+    auto node = new (_alloc.Alloc()) MPSCQueueNode<T>;
+    node->next.store(MPSCQueueNode<T>::UNCONNECTED, memory_order_relaxed);
     node->data_mem.Init(std::forward<U>(data));
     EnqueueImpl(node);
 }
@@ -117,11 +118,11 @@ template <typename T, typename Alloc>
 void MPSCQueue<T, Alloc>::EnqueueImpl(MPSCQueueNode<T>* node) {
     MPSCQueueNode<T>* prev = _head.exchange(node, memory_order_release);
     if (prev) {
-        node->next = prev;
+        node->next.store(prev, memory_order_release);
         return;
     }
-    node->next = nullptr;
-    _cur_enqueue_node.store(node, memory_order_relaxed);
+    node->next.store(nullptr, memory_order_relaxed);
+    _cur_enqueue_node.store(node, memory_order_release);
 }
 
 template <typename T, typename Alloc>
@@ -135,7 +136,7 @@ bool MPSCQueue<T, Alloc>::DequeueImpl(T* data) {
     if (_cur_dequeue_node) {
         node = _cur_dequeue_node;
     } else {
-        node = _cur_enqueue_node.exchange(nullptr, memory_order_relaxed);
+        node = _cur_enqueue_node.exchange(nullptr, memory_order_acquire);
     }
     if (!node) {
         return false;
@@ -146,10 +147,10 @@ bool MPSCQueue<T, Alloc>::DequeueImpl(T* data) {
         *data = std::move(*mem);
     }
     MPSCQueueNode<T>* old_node = node;
-    if (!node->next) {
+    if (!node->next.load(memory_order_relaxed)) {
         ReverseList(node);
     }
-    _cur_dequeue_node = node->next;
+    _cur_dequeue_node = node->next.load(memory_order_relaxed);
     node->data_mem.Destroy();
     _alloc.Free(old_node);
 
@@ -167,27 +168,27 @@ void MPSCQueue<T, Alloc>::ReverseList(MPSCQueueNode<T>* 
old_head) {
         return;
     }
     CHECK_NE(new_head, old_head);
-    // Above acquire fence pairs release fence of exchange in Enqueue() to make
-    // sure that we see all fields of requests set.
+    // Acquire the new head's initialization, including its UNCONNECTED link.
 
     // Someone added new requests.
     // Reverse the list until old_head.
     MPSCQueueNode<T>* tail = nullptr;
     MPSCQueueNode<T>* p = new_head;
     do {
-        while (p->next == MPSCQueueNode<T>::UNCONNECTED) {
-            // TODO(gejun): elaborate this
+        MPSCQueueNode<T>* saved_next;
+        // The producer connects the link after publishing the node in _head.
+        while ((saved_next = p->next.load(memory_order_acquire)) ==
+               MPSCQueueNode<T>::UNCONNECTED) {
             sched_yield();
         }
-        MPSCQueueNode<T>* const saved_next = p->next;
-        p->next = tail;
+        p->next.store(tail, memory_order_relaxed);
         tail = p;
         p = saved_next;
         CHECK(p);
     } while (p != old_head);
 
     // Link old list with new list.
-    old_head->next = tail;
+    old_head->next.store(tail, memory_order_relaxed);
 }
 
 }
diff --git a/test/mpsc_queue_unittest.cc b/test/mpsc_queue_unittest.cc
index 97fb9ee45..0a741863a 100644
--- a/test/mpsc_queue_unittest.cc
+++ b/test/mpsc_queue_unittest.cc
@@ -1,5 +1,9 @@
 #include <gtest/gtest.h>
 #include <pthread.h>
+#include <chrono>
+#include <thread>
+#include <utility>
+#include <vector>
 #include "butil/containers/mpsc_queue.h"
 
 namespace {
@@ -120,6 +124,63 @@ TEST(MPSCQueueTest, mpsc_multi_thread) {
 
 }
 
+template <typename Alloc>
+void CheckConcurrentPublication() {
+    const unsigned producer_count = 4;
+    const unsigned items_per_producer = 10000;
+    const unsigned total = producer_count * items_per_producer;
+    using Item = std::pair<unsigned, unsigned>;
+    butil::MPSCQueue<Item, Alloc> queue;
+    butil::atomic<bool> start(false);
+    std::vector<std::thread> producers;
+    for (unsigned p = 0; p < producer_count; ++p) {
+        producers.emplace_back([&, p] {
+            while (!start.load(butil::memory_order_acquire)) {
+                std::this_thread::yield();
+            }
+            for (unsigned i = 0; i < items_per_producer; ++i) {
+                const unsigned value = p * items_per_producer + i;
+                queue.Enqueue(Item(value, ~value));
+            }
+        });
+    }
+    std::vector<bool> seen(total, false);
+    unsigned consumed = 0;
+    const auto deadline = std::chrono::steady_clock::now() +
+                          std::chrono::seconds(30);
+    start.store(true, butil::memory_order_release);
+    while (consumed < total && std::chrono::steady_clock::now() < deadline) {
+        Item item;
+        if (!queue.Dequeue(item)) {
+            std::this_thread::yield();
+            continue;
+        }
+        EXPECT_EQ(~item.first, item.second);
+        EXPECT_LT(item.first, total);
+        if (item.first < total) {
+            EXPECT_FALSE(seen[item.first]);
+            seen[item.first] = true;
+        }
+        ++consumed;
+    }
+    for (auto& producer : producers) {
+        producer.join();
+    }
+    EXPECT_EQ(total, consumed);
+    Item item;
+    EXPECT_FALSE(queue.Dequeue(item));
+}
+
+TEST(MPSCQueueTest, concurrent_publication) {
+    CheckConcurrentPublication<
+        butil::DefaultAllocator<std::pair<unsigned, unsigned>>>();
+}
+
+TEST(MPSCQueueTest, concurrent_publication_with_object_pool) {
+    CheckConcurrentPublication<
+        butil::ObjectPoolAllocator<std::pair<unsigned, unsigned>>>();
+}
+
 struct MyObject {};
 
 struct DestructionTracked {


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

Reply via email to