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 8102b17d Support seqlock and use it for TaskGroup CPU time stat (#3541)
8102b17d is described below

commit 8102b17d59f39166f4c5b6e2fe98c2319eca9689
Author: Bright Chen <[email protected]>
AuthorDate: Wed Sep 16 22:37:12 2026 +0800

    Support seqlock and use it for TaskGroup CPU time stat (#3541)
---
 src/bthread/processor.h             |  28 +--
 src/bthread/task_group.cpp          | 105 +---------
 src/bthread/task_group.h            | 117 +++++------
 src/{bthread => butil}/processor.h  |  39 +---
 src/butil/synchronization/seqlock.h | 302 ++++++++++++++++++++++++++++
 test/BUILD.bazel                    |   3 +-
 test/CMakeLists.txt                 |   1 +
 test/Makefile                       |   7 +-
 test/seqlock_unittest.cpp           | 391 ++++++++++++++++++++++++++++++++++++
 9 files changed, 768 insertions(+), 225 deletions(-)

diff --git a/src/bthread/processor.h b/src/bthread/processor.h
index 06e75641..74be9e25 100644
--- a/src/bthread/processor.h
+++ b/src/bthread/processor.h
@@ -22,33 +22,7 @@
 #ifndef BTHREAD_PROCESSOR_H
 #define BTHREAD_PROCESSOR_H
 
-#include "butil/build_config.h"
-
-// Pause instruction to prevent excess processor bus usage, only works in GCC
-# ifndef cpu_relax
-#if defined(ARCH_CPU_ARM_FAMILY)
-# define cpu_relax() asm volatile("yield\n": : :"memory")
-#elif defined(ARCH_CPU_RISCV_FAMILY)
-// Use the pause hint (Zihintpause extension). Encoding 0x0100000F
-// (fence 0, 1) is a HINT on all RISC-V implementations: it never traps
-// and is ignored on CPUs without Zihintpause. On CPUs with Zihintpause
-// it provides a multi-cycle stall hint that reduces power and improves
-// resource fairness during spin-wait loops. Matches the Linux kernel's
-// RISC-V cpu_relax() behavior. .word is used instead of .insn or the
-// pause mnemonic for maximum assembler compatibility.
-# define cpu_relax() asm volatile(".word 0x0100000f\n": : :"memory")
-#elif defined(ARCH_CPU_LOONGARCH64_FAMILY)
-# define cpu_relax() asm volatile("nop\n": : :"memory");
-#else
-# define cpu_relax() asm volatile("pause\n": : :"memory")
-#endif
-# endif
-
-// Compile read-write barrier
-# ifndef barrier
-# define barrier() asm volatile("": : :"memory")
-# endif
-
+#include "butil/processor.h"
 
 # define BT_LOOP_WHEN(expr, num_spins)                                  \
     do {                                                                \
diff --git a/src/bthread/task_group.cpp b/src/bthread/task_group.cpp
index 5b02f8c9..0503fa4f 100644
--- a/src/bthread/task_group.cpp
+++ b/src/bthread/task_group.cpp
@@ -84,103 +84,6 @@ BAIDU_VOLATILE_THREAD_LOCAL(void*, tls_unique_user_ptr, 
nullptr);
 
 const TaskStatistics EMPTY_STAT = { 0, 0, 0 };
 
-AtomicInteger128::Value AtomicInteger128::load() const {
-#ifdef __x86_64__
-    (void)_mutex;
-    (void)_seq;
-    __m128i value = _mm_load_si128(reinterpret_cast<const __m128i*>(&_value));
-    return {value[0], value[1]};
-#elif defined(__ARM_NEON)
-    (void)_mutex;
-    (void)_seq;
-    int64x2_t value = vld1q_s64(reinterpret_cast<const int64_t*>(&_value));
-    return {value[0], value[1]};
-#elif defined(__riscv) && __riscv_xlen == 64
-    (void)_mutex;
-    // RISC-V: Seqlock-based atomic 128-bit load.
-    int64_t v1, v2;
-    uint64_t seq0, seq1;
-    do {
-        __asm__ volatile(
-            "ld %0, %1\n\t"
-            : "=r"(seq0)
-            : "m"(_seq)
-            : "memory"
-        );
-        if (seq0 & 1) continue;
-        __asm__ volatile("fence r, rw\n\t" ::: "memory");
-        __asm__ volatile(
-            "ld %0, %2\n\t"
-            "ld %1, %3\n\t"
-            : "=r"(v1), "=r"(v2)
-            : "m"(_value.v1), "m"(_value.v2)
-            : "memory"
-        );
-        __asm__ volatile("fence r, rw\n\t" ::: "memory");
-        __asm__ volatile(
-            "ld %0, %1\n\t"
-            : "=r"(seq1)
-            : "m"(_seq)
-            : "memory"
-        );
-    } while (seq0 != seq1);
-    return {v1, v2};
-#else
-    BAIDU_SCOPED_LOCK(const_cast<FastPthreadMutex&>(_mutex));
-    return _value;
-#endif
-}
-
-void AtomicInteger128::store(Value value) {
-#ifdef __x86_64__
-    (void)_seq;
-    __m128i v = _mm_load_si128(reinterpret_cast<__m128i*>(&value));
-    _mm_store_si128(reinterpret_cast<__m128i*>(&_value), v);
-#elif defined(__ARM_NEON)
-    (void)_seq;
-    int64x2_t v = vld1q_s64(reinterpret_cast<int64_t*>(&value));
-    vst1q_s64(reinterpret_cast<int64_t*>(&_value), v);
-#elif defined(__riscv) && __riscv_xlen == 64
-    (void)_mutex;
-    // RISC-V: Seqlock-based atomic 128-bit store.
-    uint64_t old_seq;
-    __asm__ volatile(
-        "ld %0, %1\n\t"
-        : "=r"(old_seq)
-        : "m"(_seq)
-        : "memory"
-    );
-    uint64_t new_seq = old_seq + 1;
-    __asm__ volatile(
-        "fence w, w\n\t"
-        "sd %1, %0\n\t"
-        : "=m"(_seq)
-        : "r"(new_seq)
-        : "memory"
-    );
-    __asm__ volatile("fence w, w\n\t" ::: "memory");
-    __asm__ volatile(
-        "sd %2, %0\n\t"
-        "sd %3, %1\n\t"
-        : "=m"(_value.v1), "=m"(_value.v2)
-        : "r"(value.v1), "r"(value.v2)
-        : "memory"
-    );
-    __asm__ volatile("fence w, w\n\t" ::: "memory");
-    new_seq++;
-    __asm__ volatile(
-        "sd %1, %0\n\t"
-        : "=m"(_seq)
-        : "r"(new_seq)
-        : "memory"
-    );
-#else
-    BAIDU_SCOPED_LOCK(const_cast<FastPthreadMutex&>(_mutex));
-    _value = value;
-#endif
-}
-
-
 int TaskGroup::get_attr(bthread_t tid, bthread_attr_t* out) {
     TaskMeta* const m = address_meta(tid);
     if (m != nullptr) {
@@ -249,7 +152,9 @@ static double get_cumulated_cputime_from_this(void* arg) {
 
 int64_t TaskGroup::cumulated_cputime_ns() const {
     CPUTimeStat cpu_time_stat = _cpu_time_stat.load();
-    // Add the elapsed time of running bthread.
+    // Add elapsed time only for a running non-main task. cpuwide_time_ns()
+    // advances while the worker is parked, so including the main task would
+    // count idle waiting as worker usage.
     int64_t cumulated_cputime_ns = cpu_time_stat.cumulated_cputime_ns();
     if (!cpu_time_stat.is_main_task()) {
         cumulated_cputime_ns += butil::cpuwide_time_ns() - 
cpu_time_stat.last_run_ns();
@@ -286,7 +191,7 @@ void TaskGroup::run_main_task() {
     }
     // Don't forget to add elapse of last wait_task.
     current_task()->stat.cputime_ns +=
-        butil::cpuwide_time_ns() - _cpu_time_stat.load_unsafe().last_run_ns();
+        butil::cpuwide_time_ns() - 
_cpu_time_stat.load_for_writer().last_run_ns();
 }
 
 TaskGroup::TaskGroup(TaskControl* c)
@@ -840,7 +745,7 @@ void TaskGroup::sched_to(TaskGroup** pg, TaskMeta* 
next_meta) {
 
     TaskMeta* const cur_meta = g->_cur_meta;
     int64_t now = butil::cpuwide_time_ns();
-    CPUTimeStat cpu_time_stat = g->_cpu_time_stat.load_unsafe();
+    CPUTimeStat cpu_time_stat = g->_cpu_time_stat.load_for_writer();
     int64_t elp_ns = now - cpu_time_stat.last_run_ns();
     cur_meta->stat.cputime_ns += elp_ns;
     // Update cpu_time_stat.
diff --git a/src/bthread/task_group.h b/src/bthread/task_group.h
index f48e02f5..bbd76ae7 100644
--- a/src/bthread/task_group.h
+++ b/src/bthread/task_group.h
@@ -22,12 +22,13 @@
 #ifndef BTHREAD_TASK_GROUP_H
 #define BTHREAD_TASK_GROUP_H
 
-#include "butil/time.h"                             // cpuwide_time_ns
+#include "butil/time.h"
+#include "butil/synchronization/seqlock.h"
 #include "bthread/task_control.h"
-#include "bthread/task_meta.h"                     // bthread_t, TaskMeta
-#include "bthread/work_stealing_queue.h"           // WorkStealingQueue
-#include "bthread/remote_task_queue.h"             // RemoteTaskQueue
-#include "butil/resource_pool.h"                    // ResourceId
+#include "bthread/task_meta.h"
+#include "bthread/work_stealing_queue.h"
+#include "bthread/remote_task_queue.h"
+#include "butil/resource_pool.h"
 #include "bthread/parking_lot.h"
 #include "bthread/prime_offset.h"
 
@@ -48,37 +49,6 @@ private:
     void* _value;
 };
 
-// Refer to https://rigtorp.se/isatomic/, On the modern CPU microarchitectures
-// (Skylake and Zen 2) AVX/AVX2 128b/256b aligned loads and stores are atomic
-// even though Intel and AMD officially doesn’t guarantee this.
-// On X86, SSE instructions can ensure atomic loads and stores.
-// Starting from Armv8.4-A, neon can ensure atomic loads and stores.
-// Otherwise, use mutex to guarantee atomicity.
-class AtomicInteger128 {
-public:
-    struct BAIDU_CACHELINE_ALIGNMENT Value {
-        int64_t v1;
-        int64_t v2;
-    };
-
-    AtomicInteger128() = default;
-    explicit AtomicInteger128(Value value) : _value(value) {}
-
-    Value load() const;
-    Value load_unsafe() const {
-        return _value;
-    }
-
-    void store(Value value);
-
-private:
-    Value _value{};
-    // Used to protect `_cpu_time_stat' on architectures without lock-free 
128-bit atomics.
-    FastPthreadMutex _mutex;
-    // Sequence counter for RISC-V seqlock implementation.
-    uint64_t _seq = 0;
-};
-
 // Thread-local group of tasks.
 // Notice that most methods involving context switching are static otherwise
 // pointer `this' may change after wakeup. The **pg parameters in following
@@ -237,66 +207,83 @@ friend class TaskControl;
 
     // Last scheduling time, task type and cumulated CPU time.
     class CPUTimeStat {
-        static constexpr int64_t LAST_SCHEDULING_TIME_MASK = 
0x7FFFFFFFFFFFFFFFLL;
-        static constexpr int64_t TASK_TYPE_MASK = 0x8000000000000000LL;
     public:
-        CPUTimeStat() : _last_run_ns_and_type(0), _cumulated_cputime_ns(0) {}
-        CPUTimeStat(AtomicInteger128::Value value)
-            : _last_run_ns_and_type(value.v1), _cumulated_cputime_ns(value.v2) 
{}
-
-        // Convert to AtomicInteger128::Value for atomic operations.
-        explicit operator AtomicInteger128::Value() const {
-            return {_last_run_ns_and_type, _cumulated_cputime_ns};
+        CPUTimeStat() : CPUTimeStat(0, 0, false) {}
+
+        CPUTimeStat(int64_t last_run_ns, int64_t cumulated_cputime_ns, bool 
main_task)
+            : _cumulated_cputime_ns(cumulated_cputime_ns)
+            , _last_run_ns(last_run_ns)
+            , _main_task(main_task) {}
+
+        CPUTimeStat(const CPUTimeStat& other)
+            : CPUTimeStat(other.last_run_ns(),
+                          other.cumulated_cputime_ns(),
+                          other.is_main_task()) {}
+
+        CPUTimeStat& operator=(const CPUTimeStat& other) {
+            if (this != &other) {
+                _last_run_ns.store(other.last_run_ns(),
+                                   butil::memory_order_relaxed);
+                _cumulated_cputime_ns.store(other.cumulated_cputime_ns(),
+                                            butil::memory_order_relaxed);
+                _main_task.store(other.is_main_task(), 
butil::memory_order_relaxed);
+            }
+            return *this;
         }
 
         void set_last_run_ns(int64_t last_run_ns, bool main_task) {
-            _last_run_ns_and_type = (last_run_ns & LAST_SCHEDULING_TIME_MASK) |
-                                    (static_cast<int64_t>(main_task) << 63);
+            _last_run_ns.store(last_run_ns, butil::memory_order_relaxed);
+            _main_task.store(main_task, butil::memory_order_relaxed);
         }
         int64_t last_run_ns() const {
-            return _last_run_ns_and_type & LAST_SCHEDULING_TIME_MASK;
-        }
-        int64_t last_run_ns_and_type() const {
-            return _last_run_ns_and_type;
+            return _last_run_ns.load(butil::memory_order_relaxed);
         }
 
         bool is_main_task() const {
-            return _last_run_ns_and_type & TASK_TYPE_MASK;
+            return _main_task.load(butil::memory_order_relaxed);
         }
 
         void add_cumulated_cputime_ns(int64_t cputime_ns, bool main_task) {
             if (main_task) {
                 return;
             }
-            _cumulated_cputime_ns += cputime_ns;
+
+            _cumulated_cputime_ns.store(cumulated_cputime_ns() + cputime_ns,
+                                        butil::memory_order_relaxed);
         }
         int64_t cumulated_cputime_ns() const {
-            return _cumulated_cputime_ns;
+            return _cumulated_cputime_ns.load(butil::memory_order_relaxed);
         }
 
     private:
-        // The higher bit for task type, main task is 1, otherwise 0.
-        // Lowest 63 bits for last scheduling time.
-        int64_t _last_run_ns_and_type;
-        // Cumulated CPU time in nanoseconds.
-        int64_t _cumulated_cputime_ns;
+        // Cumulated non-main-task elapsed time in nanoseconds.
+        butil::atomic<int64_t> _cumulated_cputime_ns;
+        butil::atomic<int64_t> _last_run_ns;
+        butil::atomic<bool> _main_task;
     };
 
     class AtomicCPUTimeStat {
     public:
         CPUTimeStat load() const {
-            return  _cpu_time_stat.load();
+            return _seqlock.load([&]() -> CPUTimeStat {
+                return _stat;
+            });
         }
-        CPUTimeStat load_unsafe() const {
-            return _cpu_time_stat.load_unsafe();
+        // For the owning writer only, with no concurrent writes. Copies fields
+        // with relaxed atomic loads but skips sequence validation.
+        CPUTimeStat load_for_writer() const {
+            return _stat;
         }
 
-        void store(CPUTimeStat cpu_time_stat) {
-            _cpu_time_stat.store(AtomicInteger128::Value(cpu_time_stat));
+        void store(const CPUTimeStat& stat) {
+            _seqlock.store([this, &stat]() {
+                _stat = stat;
+            });
         }
 
     private:
-        AtomicInteger128 _cpu_time_stat;
+        CPUTimeStat _stat;
+        butil::Seqlock<> _seqlock;
     };
 
     // You shall use TaskControl::create_group to create new instance.
diff --git a/src/bthread/processor.h b/src/butil/processor.h
similarity index 55%
copy from src/bthread/processor.h
copy to src/butil/processor.h
index 06e75641..9219d316 100644
--- a/src/bthread/processor.h
+++ b/src/butil/processor.h
@@ -15,19 +15,15 @@
 // specific language governing permissions and limitations
 // under the License.
 
-// bthread - An M:N threading library to make applications more concurrent.
-
-// Date: Fri Dec  5 13:40:57 CST 2014
-
-#ifndef BTHREAD_PROCESSOR_H
-#define BTHREAD_PROCESSOR_H
+#ifndef BUTIL_PROCESSOR_H
+#define BUTIL_PROCESSOR_H
 
 #include "butil/build_config.h"
 
 // Pause instruction to prevent excess processor bus usage, only works in GCC
-# ifndef cpu_relax
+#ifndef cpu_relax
 #if defined(ARCH_CPU_ARM_FAMILY)
-# define cpu_relax() asm volatile("yield\n": : :"memory")
+#define cpu_relax() asm volatile("yield\n": : :"memory")
 #elif defined(ARCH_CPU_RISCV_FAMILY)
 // Use the pause hint (Zihintpause extension). Encoding 0x0100000F
 // (fence 0, 1) is a HINT on all RISC-V implementations: it never traps
@@ -38,30 +34,15 @@
 // pause mnemonic for maximum assembler compatibility.
 # define cpu_relax() asm volatile(".word 0x0100000f\n": : :"memory")
 #elif defined(ARCH_CPU_LOONGARCH64_FAMILY)
-# define cpu_relax() asm volatile("nop\n": : :"memory");
+# define cpu_relax() asm volatile("nop\n": : :"memory")
 #else
 # define cpu_relax() asm volatile("pause\n": : :"memory")
 #endif
-# endif
+#endif // cpu_relax
 
 // Compile read-write barrier
-# ifndef barrier
-# define barrier() asm volatile("": : :"memory")
-# endif
-
-
-# define BT_LOOP_WHEN(expr, num_spins)                                  \
-    do {                                                                \
-        /*sched_yield may change errno*/                                \
-        const int saved_errno = errno;                                  \
-        for (int cnt = 0, saved_nspin = (num_spins); (expr); ++cnt) {   \
-            if (cnt < saved_nspin) {                                    \
-                cpu_relax();                                            \
-            } else {                                                    \
-                sched_yield();                                          \
-            }                                                           \
-        }                                                               \
-        errno = saved_errno;                                            \
-    } while (0)
+#ifndef barrier
+#define barrier() asm volatile("": : :"memory")
+#endif // barrier
 
-#endif // BTHREAD_PROCESSOR_H
+#endif // BUTIL_PROCESSOR_H
\ No newline at end of file
diff --git a/src/butil/synchronization/seqlock.h 
b/src/butil/synchronization/seqlock.h
new file mode 100644
index 00000000..3e91dcb5
--- /dev/null
+++ b/src/butil/synchronization/seqlock.h
@@ -0,0 +1,302 @@
+// 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.
+
+#ifndef BUTIL_SYNCHRONIZATION_SEQLOCK_H
+#define BUTIL_SYNCHRONIZATION_SEQLOCK_H
+
+#include <stdint.h>
+#include <functional>
+#include <mutex>
+#include <type_traits>
+#include <utility>
+
+#include "butil/atomicops.h"
+#include "butil/compiler_specific.h"
+#include "butil/macros.h"
+#include "butil/processor.h"
+
+namespace butil {
+namespace internal {
+
+// Detects std::reference_wrapper<T>.
+template <typename T>
+struct IsReferenceWrapper : std::false_type {};
+template <typename T>
+struct IsReferenceWrapper<std::reference_wrapper<T>> : std::true_type {};
+
+// A sequence counter for consistent reads without acquiring a reader mutex
+// around caller-owned payloads. This is an implementation detail of 
butil::Seqlock;
+// use butil::Seqlock instead.
+//
+// SeqCounter does not protect payload accesses from C++ data races. Callers 
must
+// access shared payloads atomically (typically with relaxed ordering) or use
+// another mechanism that makes concurrent accesses valid.
+//
+//
+// Cache-line aligned so the sequence counter does not falsely share a line
+// with adjacent data (e.g. the writer mutex in Seqlock<Mutex> or a 
neighbouring
+// payload), which would bounce the line between readers and writers.
+class BAIDU_CACHELINE_ALIGNMENT SeqCounter {
+public:
+    SeqCounter() : _seq(0) {}
+    DISALLOW_COPY_AND_ASSIGN(SeqCounter);
+
+    // Repeatedly invoke `load_payload` until it observes one consistent 
version.
+    // `load_payload` may run several times when it races with a writer, so it
+    // must be cheap and side-effect free: only read the shared payload and
+    // return a copy, never mutate observable state. It must also access the
+    // shared payload without causing a C++ data race (relaxed atomics).
+    //
+    // The callback MUST return an owning value (a copy of the data), never a
+    // pointer, reference, or view (string_view, span, reference_wrapper, ...)
+    // into the payload. The consistency guarantee covers only the bytes copied
+    // out before the validating load: once load() returns, a later writer may
+    // mutate the payload, so any handle that still points into it no longer
+    // refers to a consistent snapshot.
+    template <typename Load>
+    typename std::decay<decltype(std::declval<Load&>()())>::type
+    load(Load&& load_payload) const {
+        typedef typename std::decay<decltype(std::declval<Load&>()())>::type 
Result;
+        static_assert(!std::is_void<Result>::value,
+                      "SeqCounter load callback must return a value");
+        static_assert(!std::is_pointer<Result>::value,
+                      "SeqCounter load callback must return an owning value, 
not "
+                      "a pointer into the payload: the pointee can be mutated 
by "
+                      "a later writer, so it is not a consistent snapshot");
+        static_assert(!IsReferenceWrapper<Result>::value,
+                      "SeqCounter load callback must return an owning value, 
not "
+                      "a std::reference_wrapper into the payload: the referent 
"
+                      "can be mutated by a later writer, so it is not a "
+                      "consistent snapshot");
+
+        while (true) {
+            // Wait for any active writer, then read on an even sequence.
+            uint64_t seq;
+            while ((seq = _seq.load(memory_order_acquire)) & 1) {
+                cpu_relax();
+            }
+
+            Result result = load_payload();
+
+            // Keep the payload reads above before the sequence validation
+            // below; retry if a writer intervened.
+            atomic_thread_fence(memory_order_acquire);
+            if (_seq.load(memory_order_relaxed) == seq) {
+                return result;
+            }
+        }
+    }
+
+    // Invoke a payload writer inside one write section. Writers must be 
serialized
+    // externally and must not nest write sections. The section is closed via 
RAII
+    // even if `store_payload` throws, so an exception does not leave the 
sequence
+    // permanently odd. A throwing writer may leave the payload partially 
updated,
+    // so callers must ensure any partial state is still safe (non-crashing) 
to read.
+    template <typename Store>
+    void store(Store&& store_payload) {
+        WriteGuard guard(*this);
+        store_payload();
+    }
+
+private:
+    // RAII scope for a write section.
+    class WriteGuard {
+    public:
+        explicit WriteGuard(SeqCounter& sc)
+            : _seq_counter(&sc)
+            , _next_seq(sc._seq.load(memory_order_relaxed) + 2) {
+            // Writers are serialized, so no atomic read-modify-write is 
needed.
+            // Keep the next even sequence for publication when the guard 
exits.
+            _seq_counter->_seq.store(_next_seq - 1, memory_order_relaxed);
+            // Order the odd-sequence store before the payload stores that
+            // follow. A release fence is a StoreStore (+LoadStore) barrier: it
+            // keeps any store after the fence (the payload writes) from being
+            // reordered ahead of stores before it (the odd sequence), so no
+            // reader can observe a payload write without also observing the 
odd
+            // sequence. Equivalently, this release fence pairs -- through the
+            // payload atomics -- with the acquire fence in load(): if a reader
+            // reads a payload value published after this fence, that fence
+            // synchronizes-with the reader's acquire fence, so the reader is
+            // guaranteed to also see the odd sequence on its validating load
+            // and retry. The acquire half of an acq_rel fence would add
+            // nothing here (no prior load needs ordering), so release alone is
+            // sufficient and states the intent precisely.
+            atomic_thread_fence(memory_order_release);
+        }
+
+        DISALLOW_COPY_AND_ASSIGN(WriteGuard);
+
+        ~WriteGuard() {
+            // Publish the payload stores and leave the write section.
+            _seq_counter->_seq.store(_next_seq, memory_order_release);
+        }
+
+    private:
+        SeqCounter* _seq_counter;
+        uint64_t _next_seq;
+    };
+
+    atomic<uint64_t> _seq;
+};
+
+}  // namespace internal
+
+// A sequence lock built on top of internal::SeqCounter.
+//
+// Seqlock<> : single-writer, no writer mutex. Backed directly by SeqCounter;
+//             the caller must serialize writers with appropriate 
synchronization
+//             if ownership is transferred between threads.
+//
+// Seqlock<Mutex> : multi-writer. Owns a writer Mutex so that concurrent
+//                  store() calls are serialized automatically (this is the
+//                  Linux seqlock_t = SeqCounter + lock).
+//
+// In both forms readers do not acquire a mutex or prevent writers from 
entering
+// a write section. Reads are NOT lock-free: they spin while the sequence is 
odd
+// and retry if a write intervenes. A suspended writer can stall all readers, 
and
+// continuous writes can starve readers; there is no bounded completion time.
+//
+// REQUIRED: write sections must not nest. A store() callback must not call 
load()
+// or store() on the same Seqlock, or wait for work that needs to read or 
write it.
+// In particular, a signal handler must not access the same Seqlock when it has
+// interrupted a writer: the interrupted write cannot finish until the handler
+// returns, so a reader in the handler would spin forever. Keep write sections
+// short and avoid blocking or yielding inside them.
+//
+// Example: publish a (x, y) pair atomically so readers never see a torn mix.
+//
+//   // the caller-owned payload
+//   struct Point {
+//       butil::atomic<int64_t> x{0};
+//       butil::atomic<int64_t> y{0};
+//   };
+//   Point point;
+//   // single writer; use Seqlock<Mutex> if several threads may write.
+//   butil::Seqlock<> seqlock;
+//
+//   // Writer: the whole update is published as one consistent version.
+//   void set(int64_t x, int64_t y) {
+//       seqlock.store([&] {
+//           point.x.store(x, butil::memory_order_relaxed);
+//           point.y.store(y, butil::memory_order_relaxed);
+//       });
+//   }
+//
+//   // Reader: load() retries internally until it copies out one consistent
+//   // version, then returns whatever the callback returned.
+//   std::pair<int64_t, int64_t> get() {
+//       return seqlock.load([&] {
+//           return std::make_pair(point.x.load(butil::memory_order_relaxed),
+//                                 point.y.load(butil::memory_order_relaxed));
+//       });
+//   }
+//
+// REQUIRED: the payload accessed inside the load/store callbacks MUST be
+// atomic (e.g. butil::atomic fields, typically read/written with
+// memory_order_relaxed). This is not just a style preference -- the reader
+// deliberately reads the payload while a writer may be mutating it, so the
+// accesses are concurrent by design. Correctness relies on it in two ways:
+//
+//   1. Data race / UB. A non-atomic object read while another thread writes it
+//      is a C++ data race, i.e. undefined behavior. The compiler is then free
+//      to tear or fuse the access, invent extra reads, or hoist/sink it across
+//      the fences below. The sequence-validation retry cannot rescue this: it
+//      only tells you *whether* to retry, it cannot un-corrupt a value the
+//      compiler already mangled, so you may return garbage even on a "clean"
+//      (seq unchanged) read.
+//   2. Ordering. The release fence on the write side pairs with the acquire 
fence
+//      on the read side through the payload atomics (fence-fence 
synchronization).
+//      If the payload is not atomic that pairing does not hold, so "observe a
+//      payload write => observe the odd sequence and retry" is no longer 
guaranteed
+//      and a reader can silently accept a stale or half-written snapshot.
+//
+// If you cannot make the payload atomic, do not use Seqlock -- use a Mutex or
+// an RWLock instead.
+//
+// REQUIRED: the load() callback must return an OWNING value (a copy of the
+// data), never a pointer, reference, or view (string_view, span,
+// reference_wrapper, ...) into the payload. load() only guarantees that the
+// bytes copied out before its validating load are consistent; after load()
+// returns a writer may mutate the payload again, so any handle still pointing
+// into it is no longer a consistent snapshot.
+//
+// Best fit: a small payload that is read far more often than written. The read
+// path copies the whole payload out and retries the copy whenever a write 
races
+// it, so a large payload makes both the copy and the retries expensive. For a
+// large or heap-owning payload prefer a RCU/RWLock scheme instead.
+template <typename Mutex = void>
+class Seqlock;
+
+// Single-writer specialization: no mutex, delegates straight to SeqCounter.
+template <>
+class Seqlock<void> {
+public:
+    Seqlock() = default;
+    DISALLOW_COPY_AND_ASSIGN(Seqlock);
+
+    // Consistent read without acquiring a mutex; may spin/retry indefinitely.
+    template <typename Load>
+    typename std::decay<decltype(std::declval<Load&>()())>::type
+    load(Load&& load_payload) const {
+        return _seq.load(std::forward<Load>(load_payload));
+    }
+
+    // Single writer only: the caller MUST ensure there is no concurrent or 
nested
+    // store(). The write section is closed via RAII even if store_payload
+    // throws.
+    template <typename Store>
+    void store(Store&& store_payload) {
+        _seq.store(std::forward<Store>(store_payload));
+    }
+
+private:
+    internal::SeqCounter _seq;
+};
+
+// Multi-writer specialization: SeqCounter + a writer Mutex.
+//
+// Mutex must be default-constructible and satisfy the C++ Lockable
+// requirements (lock()/unlock()), e.g. butil::Mutex.
+template <typename Mutex>
+class Seqlock {
+public:
+    Seqlock() = default;
+    DISALLOW_COPY_AND_ASSIGN(Seqlock);
+
+    // Consistent read without acquiring a mutex; may spin/retry indefinitely.
+    template <typename Load>
+    typename std::decay<decltype(std::declval<Load&>()())>::type
+    load(Load&& load_payload) const {
+        return _seq.load(std::forward<Load>(load_payload));
+    }
+
+    // Serialized write: acquires the mutex, then runs the payload writer in
+    // one write section.
+    template <typename Store>
+    void store(Store&& store_payload) {
+        std::lock_guard<Mutex> lk(_mutex);
+        _seq.store(std::forward<Store>(store_payload));
+    }
+
+private:
+    internal::SeqCounter _seq;
+    Mutex _mutex;
+};
+
+}  // namespace butil
+
+#endif  // BUTIL_SYNCHRONIZATION_SEQLOCK_H
diff --git a/test/BUILD.bazel b/test/BUILD.bazel
index 8706f63f..1aecc399 100644
--- a/test/BUILD.bazel
+++ b/test/BUILD.bazel
@@ -125,9 +125,10 @@ TEST_BUTIL_SOURCES = [
     "recordio_unittest.cpp",
     #"popen_unittest.cpp",
     "bounded_queue_unittest.cc",
-    "butil_unittest_main.cpp",
     "scope_guard_unittest.cpp",
     "optional_unittest.cpp",
+    "seqlock_unittest.cpp",
+    "butil_unittest_main.cpp",
 ] + select({
     "@bazel_tools//tools/osx:darwin_x86_64": [],
     "//conditions:default": [
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 6aebe271..71baf85d 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -181,6 +181,7 @@ SET(TEST_BUTIL_SOURCES
     ${PROJECT_SOURCE_DIR}/test/scoped_locale.cc
     ${PROJECT_SOURCE_DIR}/test/scope_guard_unittest.cpp
     ${PROJECT_SOURCE_DIR}/test/optional_unittest.cpp
+    ${PROJECT_SOURCE_DIR}/test/seqlock_unittest.cpp
     ${PROJECT_SOURCE_DIR}/test/butil_unittest_main.cpp
        )
 
diff --git a/test/Makefile b/test/Makefile
index f2348e7f..de4d566b 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -150,9 +150,10 @@ TEST_BUTIL_SOURCES = \
     scoped_locale.cc \
     popen_unittest.cpp \
     bounded_queue_unittest.cc \
-    butil_unittest_main.cpp \
-    scope_guard_unittest.cpp \
-    optional_unittest.cpp
+       scope_guard_unittest.cpp \
+       optional_unittest.cpp \
+       seqlock_unittest.cpp \
+    butil_unittest_main.cpp
 
 ifeq ($(SYSTEM), Linux)
     TEST_BUTIL_SOURCES += test_file_util_linux.cc \
diff --git a/test/seqlock_unittest.cpp b/test/seqlock_unittest.cpp
new file mode 100644
index 00000000..85ab4e9c
--- /dev/null
+++ b/test/seqlock_unittest.cpp
@@ -0,0 +1,391 @@
+// 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 <unistd.h>
+#include <gtest/gtest.h>
+#include <pthread.h>
+#include <mutex>
+#include <cstdio>
+#include "butil/atomicops.h"
+#include "butil/time.h"
+#include "butil/synchronization/lock.h"
+#include "butil/synchronization/seqlock.h"
+
+namespace {
+
+// A multi-word payload. The seqlock's job is to make every reader observe a
+// snapshot in which all words are equal; a torn read would see a mix of an old
+// and a new value. Every field is a relaxed atomic, as Seqlock requires.
+static const int kWords = 8;
+struct Payload {
+    butil::atomic<uint64_t> w[kWords];
+
+    void relaxed_set(uint64_t v) {
+        for (auto& i : w) {
+            // Store word by word (not as one atomic group) so that, without 
the
+            // seqlock, a concurrent reader could observe a torn value.
+            i.store(v, butil::memory_order_relaxed);
+        }
+    }
+    // Returns the first word and whether all words are equal to it.
+    uint64_t relaxed_get(bool* consistent) const {
+        uint64_t v0 = w[0].load(butil::memory_order_relaxed);
+        *consistent = true;
+        for (int i = 1; i < kWords; ++i) {
+            if (w[i].load(butil::memory_order_relaxed) != v0) {
+                *consistent = false;
+            }
+        }
+        return v0;
+    }
+};
+
+TEST(SeqlockTest, SingleThreadedReadWrite) {
+    butil::Seqlock<> seqlock;
+    Payload payload;
+    payload.relaxed_set(0);
+
+    for (uint64_t v = 1; v <= 1000; ++v) {
+        seqlock.store([&] { payload.relaxed_set(v); });
+        uint64_t got = seqlock.load([&] {
+            bool consistent = false;
+            uint64_t r = payload.relaxed_get(&consistent);
+            EXPECT_TRUE(consistent);
+            return r;
+        });
+        ASSERT_EQ(v, got);
+    }
+}
+
+TEST(SeqlockTest, LoadReturnsValueByType) {
+    butil::Seqlock<> seqlock;
+    butil::atomic<int> payload(0);
+    seqlock.store([&] {
+        payload.store(42, butil::memory_order_relaxed);
+    });
+    // load returns whatever the callback returns, by value.
+    int v = seqlock.load([&] {
+        return payload.load(butil::memory_order_relaxed);
+    });
+    ASSERT_EQ(42, v);
+    // A different return type also works.
+    std::pair<int, int> pr = seqlock.load([&] {
+        int v = payload.load(butil::memory_order_relaxed);
+        return std::make_pair(v, v + 1);
+    });
+    ASSERT_EQ(42, pr.first);
+    ASSERT_EQ(43, pr.second);
+}
+
+TEST(SeqlockTest, MutexSpecializationSingleThreaded) {
+    butil::Seqlock<butil::Mutex> seqlock;
+    Payload payload;
+    payload.relaxed_set(0);
+    for (uint64_t v = 1; v <= 1000; ++v) {
+        seqlock.store([&] { payload.relaxed_set(v); });
+        bool consistent = false;
+        uint64_t got = seqlock.load([&] {
+            bool c = false;
+            uint64_t r = payload.relaxed_get(&c);
+            consistent = c;
+            return r;
+        });
+        ASSERT_TRUE(consistent);
+        ASSERT_EQ(v, got);
+    }
+}
+
+// Single-threaded performance comparison: Seqlock vs Mutex.
+//
+// There is no contention here, so this measures the bare cost of the
+// synchronization itself:
+//   * Seqlock<>       : one relaxed counter load, two counter stores and a
+//                       release fence on the write path; two counter loads and
+//                       an acquire fence on the read path (no atomic RMW in
+//                       either path).
+//   * Seqlock<Mutex>  : the same counter work plus a lock/unlock pair on the
+//                       write path; the read path is identical to Seqlock<>.
+//   * Mutex           : a lock/unlock pair (atomic RMW + potential syscall on
+//                       contention) on BOTH the write and the read path.
+//
+// Timings are reported, not asserted: absolute numbers depend on the machine,
+// the compiler and the current load, so turning them into thresholds would
+// make the test flaky. The test only asserts that the measurements ran and
+// produced the expected values.
+static const int kPerfRounds = 1000000;
+
+// Accumulates every value read so the compiler cannot optimize the read loops
+// away as dead code.
+static butil::atomic<uint64_t> g_perf_sink(0);
+
+inline void perf_consume(uint64_t v) {
+    g_perf_sink.fetch_add(v, butil::memory_order_relaxed);
+}
+
+// A pure compiler barrier: it emits no instruction, but forbids the compiler
+// from moving memory accesses across it. Without one between the rounds below,
+// a write body made purely of relaxed stores is legally collapsible into its
+// last iteration -- which is exactly what happens to Seqlock<> once its 
counter
+// increments are plain stores rather than fetch_add, and it measures 0 ns/op.
+// The mutex bodies resist that on their own; barriering all of them keeps the
+// comparison between the three honest.
+inline void perf_barrier() {
+    butil::atomic_signal_fence(butil::memory_order_seq_cst);
+}
+
+// Runs `body(round)` for `rounds` rounds and returns the elapsed nanoseconds.
+// A short warmup runs first so that page faults, branch predictor and cache
+// warmup are not charged to the measured loop.
+template <typename Body>
+int64_t TimeLoopNs(int rounds, Body&& body) {
+    int kWarmup = 1000;
+    for (int i = 1; i <= kWarmup; ++i) {
+        body(static_cast<uint64_t>(i));
+        perf_barrier();
+    }
+    int64_t start_ns = butil::cpuwide_time_ns();
+    for (int i = 1; i <= rounds; ++i) {
+        body(static_cast<uint64_t>(i));
+        perf_barrier();
+    }
+    return butil::cpuwide_time_ns() - start_ns;
+}
+
+inline double ns_per_op(int64_t elapsed_ns, int rounds) {
+    return static_cast<double>(elapsed_ns) / rounds;
+}
+
+TEST(SeqlockTest, SingleThreadedPerfVsMutex) {
+    // Seqlock<>: single-writer, no mutex.
+    butil::Seqlock<> seqlock;
+    Payload seqlock_payload;
+    seqlock_payload.relaxed_set(0);
+    int64_t seqlock_write_ns = TimeLoopNs(kPerfRounds, [&](uint64_t v) {
+        seqlock.store([&] { seqlock_payload.relaxed_set(v); });
+        // Make both the counter and payload observable after every write so
+        // the compiler cannot eliminate stores to these local test objects.
+        asm volatile("" : : "m"(seqlock), "m"(seqlock_payload) : "memory");
+    });
+    int64_t seqlock_read_ns = TimeLoopNs(kPerfRounds, [&](uint64_t) {
+        perf_consume(seqlock.load([&] {
+            bool consistent = false;
+            return seqlock_payload.relaxed_get(&consistent);
+        }));
+    });
+
+    // Seqlock<Mutex>: same read path, writes additionally take a mutex.
+    butil::Seqlock<butil::Mutex> mutex_seqlock;
+    Payload mutex_seqlock_payload;
+    mutex_seqlock_payload.relaxed_set(0);
+    int64_t mutex_seqlock_write_ns = TimeLoopNs(kPerfRounds, [&](uint64_t v) {
+        mutex_seqlock.store([&] { mutex_seqlock_payload.relaxed_set(v); });
+        asm volatile("" : : "m"(mutex_seqlock), "m"(mutex_seqlock_payload)
+                     : "memory");
+    });
+    int64_t mutex_seqlock_read_ns = TimeLoopNs(kPerfRounds, [&](uint64_t) {
+        perf_consume(mutex_seqlock.load([&] {
+            bool consistent = false;
+            return mutex_seqlock_payload.relaxed_get(&consistent);
+        }));
+    });
+
+    // Plain Mutex baseline: both sides take the lock.
+    butil::Mutex mutex;
+    Payload mutex_payload;
+    mutex_payload.relaxed_set(0);
+    int64_t mutex_write_ns = TimeLoopNs(kPerfRounds, [&](uint64_t v) {
+        {
+            std::lock_guard<butil::Mutex> lk(mutex);
+            mutex_payload.relaxed_set(v);
+        }
+        asm volatile("" : : "m"(mutex), "m"(mutex_payload) : "memory");
+    });
+    int64_t mutex_read_ns = TimeLoopNs(kPerfRounds, [&](uint64_t) {
+        std::lock_guard<butil::Mutex> lk(mutex);
+        bool consistent = false;
+        perf_consume(mutex_payload.relaxed_get(&consistent));
+    });
+
+    printf("\n[seqlock perf] single thread, %d rounds, payload=%d words\n",
+           kPerfRounds, kWords);
+    printf("%-18s %14s %14s\n", "impl", "write(ns/op)", "read(ns/op)");
+    printf("%-18s %14.2f %14.2f\n", "Seqlock<>",
+           ns_per_op(seqlock_write_ns, kPerfRounds),
+           ns_per_op(seqlock_read_ns, kPerfRounds));
+    printf("%-18s %14.2f %14.2f\n", "Seqlock<Mutex>",
+           ns_per_op(mutex_seqlock_write_ns, kPerfRounds),
+           ns_per_op(mutex_seqlock_read_ns, kPerfRounds));
+    printf("%-18s %14.2f %14.2f\n", "Mutex",
+           ns_per_op(mutex_write_ns, kPerfRounds),
+           ns_per_op(mutex_read_ns, kPerfRounds));
+    printf("%-18s %13.2fx %13.2fx\n", "Mutex/Seqlock<>",
+           static_cast<double>(mutex_write_ns) / seqlock_write_ns,
+           static_cast<double>(mutex_read_ns) / seqlock_read_ns);
+
+    // The loops really ran and every implementation published the last value.
+    ASSERT_GT(seqlock_write_ns, 0);
+    ASSERT_GT(mutex_seqlock_write_ns, 0);
+    ASSERT_GT(mutex_write_ns, 0);
+    ASSERT_GT(seqlock_read_ns, 0);
+    ASSERT_GT(mutex_seqlock_read_ns, 0);
+    ASSERT_GT(mutex_read_ns, 0);
+    ASSERT_GT(g_perf_sink.load(butil::memory_order_relaxed), 0u);
+
+    bool consistent = false;
+    ASSERT_EQ(static_cast<uint64_t>(kPerfRounds),
+              seqlock_payload.relaxed_get(&consistent));
+    ASSERT_TRUE(consistent);
+    ASSERT_EQ(static_cast<uint64_t>(kPerfRounds),
+              mutex_seqlock_payload.relaxed_get(&consistent));
+    ASSERT_TRUE(consistent);
+    ASSERT_EQ(static_cast<uint64_t>(kPerfRounds),
+              mutex_payload.relaxed_get(&consistent));
+    ASSERT_TRUE(consistent);
+}
+
+// Concurrent consistency tests
+struct SharedState {
+    butil::Seqlock<>* single_writer_seqlock = nullptr; // single-writer lock
+    butil::Seqlock<butil::Mutex>* multi_writer_seqlock = nullptr; // 
multi-writer lock
+    Payload payload;
+    butil::atomic<bool> stopped{false};
+    butil::atomic<uint64_t> version{0}; // source of the value written
+    butil::atomic<uint64_t> reads{0}; // total reads performed
+    butil::atomic<uint64_t> torn{0}; // inconsistent snapshots observed
+};
+
+void* SingleWriterThread(void* arg) {
+    SharedState* shared_state = static_cast<SharedState*>(arg);
+    while (!shared_state->stopped.load(butil::memory_order_relaxed)) {
+        uint64_t version = shared_state->version.fetch_add(1, 
butil::memory_order_relaxed) + 1;
+        shared_state->single_writer_seqlock->store([&] {
+            shared_state->payload.relaxed_set(version);
+        });
+    }
+    return nullptr;
+}
+
+void* MultiWriterThread(void* arg) {
+    SharedState* shared_state = static_cast<SharedState*>(arg);
+    while (!shared_state->stopped.load(butil::memory_order_relaxed)) {
+        uint64_t version = shared_state->version.fetch_add(1, 
butil::memory_order_relaxed) + 1;
+        shared_state->multi_writer_seqlock->store([&] {
+            shared_state->payload.relaxed_set(version);
+        });
+    }
+    return nullptr;
+}
+
+struct ReaderArg {
+    SharedState* shared_state;
+    bool multi_writer;
+};
+
+void* ReaderThread(void* arg) {
+    ReaderArg* reader_arg = static_cast<ReaderArg*>(arg);
+    SharedState* shared_state = reader_arg->shared_state;
+    uint64_t local_reads = 0;
+    uint64_t local_torn = 0;
+    while (!shared_state->stopped.load(butil::memory_order_relaxed)) {
+        bool consistent = false;
+        auto load_body = [&] {
+            bool c = false;
+            uint64_t r = shared_state->payload.relaxed_get(&c);
+            consistent = c;
+            return r;
+        };
+        if (reader_arg->multi_writer) {
+            shared_state->multi_writer_seqlock->load(load_body);
+        } else {
+            shared_state->single_writer_seqlock->load(load_body);
+        }
+        ++local_reads;
+        if (!consistent) {
+            ++local_torn;
+        }
+    }
+    shared_state->reads.fetch_add(local_reads, butil::memory_order_relaxed);
+    shared_state->torn.fetch_add(local_torn, butil::memory_order_relaxed);
+    return nullptr;
+}
+
+TEST(SeqlockTest, SingleWriterManyReaders) {
+    SharedState shared_state;
+    butil::Seqlock<> sl;
+    shared_state.single_writer_seqlock = &sl;
+    shared_state.payload.relaxed_set(0);
+
+    const int kReaders = 4;
+    pthread_t writer;
+    pthread_t readers[kReaders];
+    ReaderArg args[kReaders];
+
+    ASSERT_EQ(0, pthread_create(&writer, nullptr, SingleWriterThread, 
&shared_state));
+    for (int i = 0; i < kReaders; ++i) {
+        args[i].shared_state = &shared_state;
+        args[i].multi_writer = false;
+        ASSERT_EQ(0, pthread_create(&readers[i], nullptr, ReaderThread, 
&args[i]));
+    }
+
+    usleep(500 * 1000);  // 0.5s of hammering
+    shared_state.stopped.store(true, butil::memory_order_relaxed);
+
+    pthread_join(writer, nullptr);
+    for (auto reader : readers) {
+        pthread_join(reader, nullptr);
+    }
+
+    ASSERT_GT(shared_state.reads.load(), 0u);
+    ASSERT_EQ(0u, shared_state.torn.load()) << "readers observed torn 
snapshots";
+}
+
+TEST(SeqlockTest, MultiWriterManyReaders) {
+    SharedState shared_state;
+    butil::Seqlock<butil::Mutex> seqlock;
+    shared_state.multi_writer_seqlock = &seqlock;
+    shared_state.payload.relaxed_set(0);
+
+    const int kWriters = 3;
+    const int kReaders = 4;
+    pthread_t writers[kWriters];
+    pthread_t readers[kReaders];
+    ReaderArg args[kReaders];
+
+    for (auto& writer : writers) {
+        ASSERT_EQ(0, pthread_create(&writer, nullptr, MultiWriterThread, 
&shared_state));
+    }
+    for (int i = 0; i < kReaders; ++i) {
+        args[i].shared_state = &shared_state;
+        args[i].multi_writer = true;
+        ASSERT_EQ(0, pthread_create(&readers[i], nullptr, ReaderThread, 
&args[i]));
+    }
+
+    usleep(500 * 1000);
+    shared_state.stopped.store(true, butil::memory_order_relaxed);
+
+    for (auto writer : writers) {
+        pthread_join(writer, nullptr);
+    }
+    for (auto reader : readers) {
+        pthread_join(reader, nullptr);
+    }
+
+    ASSERT_GT(shared_state.reads.load(), 0u);
+    ASSERT_EQ(0u, shared_state.torn.load()) << "readers observed torn 
snapshots";
+}
+
+}  // namespace


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

Reply via email to