我正在研究Seqlock的实现。然而,我找到的所有来源都有不同的实现方法。
Linux内核
static inline unsigned __read_seqcount_begin(const seqcount_t *s)
{
unsigned ret;
repeat:
ret = READ_ONCE(s->sequence);
if (unlikely(ret & 1)) {
cpu_relax();
goto repeat;
}
return ret;
}
static inline unsigned raw_read_seqcount_begin(const seqcount_t *s)
{
unsigned ret = __read_seqcount_begin(s);
smp_rmb();
return ret;
}
基本上,它在读取器端使用了一次易失性读取和具有获取语义的读取屏障。使用时,后续读取是不受保护的。
struct Data {
u64 a, b;
};
// ...
read_seqcount_begin(&seq);
int v1 = d.a, v2 = d.b;
// ...
rigtorp/Seqlock
RIGTORP_SEQLOCK_NOINLINE T load() const noexcept {
T copy;
std::size_t seq0, seq1;
do {
seq0 = seq_.load(std::memory_order_acquire);
std::atomic_signal_fence(std::memory_order_acq_rel);
copy = value_;
std::atomic_signal_fence(std::memory_order_acq_rel);
seq1 = seq_.load(std::memory_order_acquire);
} while (seq0 != seq1 || seq0 & 1);
return copy;
}
数据负载仍然没有进行原子操作或保护。但是,在读取之前添加了具有获取-释放语义的
atomic_signal_fence
,与内核中具有获取语义的rmb
相反。
Amanieu/seqlock(Rust)
pub fn read(&self) -> T {
loop {
// Load the first sequence number. The acquire ordering ensures that
// this is done before reading the data.
let seq1 = self.seq.load(Ordering::Acquire);
// If the sequence number is odd then it means a writer is currently
// modifying the value.
if seq1 & 1 != 0 {
// Yield to give the writer a chance to finish. Writing is
// expected to be relatively rare anyways so this isn't too
// performance critical.
thread::yield_now();
continue;
}
// We need to use a volatile read here because the data may be
// concurrently modified by a writer.
let result = unsafe { ptr::read_volatile(self.data.get()) };
// Make sure the seq2 read occurs after reading the data. What we
// ideally want is a load(Release), but the Release ordering is not
// available on loads.
fence(Ordering::Acquire);
// If the sequence number is the same then the data wasn't modified
// while we were reading it, and can be returned.
let seq2 = self.seq.load(Ordering::Relaxed);
if seq1 == seq2 {
return result;
}
}
}
在加载 seq
和 data
之间没有内存屏障,而是使用了一个 volatile 读取。
Seqlock 能否与编程语言内存模型兼容?(第三个变体)
T reader() {
int r1, r2;
unsigned seq0, seq1;
do {
seq0 = seq.load(m_o_acquire);
r1 = data1.load(m_o_relaxed);
r2 = data2.load(m_o_relaxed);
atomic_thread_fence(m_o_acquire);
seq1 = seq.load(m_o_relaxed);
} while (seq0 != seq1 || seq0 & 1);
// do something with r1 and r2;
}
与Rust实现类似,但是数据上使用原子操作而不是
volatile_read
。
P1478R1: 字节级原子memcpy中的参数
该论文声称:
在一般情况下,有很好的语义理由要求在这样一个seqlock“关键段”内的所有数据访问都必须是原子的。如果我们读取指针p作为读取数据的一部分,然后也读取*p,如果p的读取恰好看到了半更新的指针值,则关键段内的代码可能会从错误的地址读取。在这种情况下,可能没有办法避免使用传统的原子加载读取指针,这正是所需的。
然而,在许多情况下,特别是在多进程情况下,seqlock数据由单个可以简单复制的对象组成,而seqlock“关键段”由一个简单的复制操作组成。在正常情况下,这可以使用memcpy来编写。但这在这里是不可接受的,因为memcpy不生成原子访问,并且(根据我们的规范)容易受到数据竞争的影响。
目前,要正确地编写这样的代码,我们需要将这些数据基本上分解成许多小型无锁原子子对象,并逐个复制它们。将数据视为单个大型原子对象将破坏seqlock的目的,因为原子复制操作将获取传统锁定。我们的提案基本上添加了一个方便的库设施来自动化这种分解成小对象的过程。
我的问题
- 上述实现中哪些是正确的?哪些是正确但效率低下的?
volatile_read
能否在seqlock的acquire-read之前重新排序?
atomic<long>
的块(具有mo_relaxed
)和一个恰当放置的atomic_thread_fence(mo_acquire)
来编写符合该模型的 seqlock。但是,这可能会破坏一些优化,特别是如果数据足够大,值得使用 SIMD 向量来复制,因此人们通常会滥用事实上的正常行为。 - Peter Cordesmemory_order_seq_cst
的加载。这是允许的吗?(影响seqlocks的gcc错误)/将fetch_add(0,memory_order_relaxed/release)转换为mfence + mov是否合法?(“N4455没有理智的编译器会优化原子操作”SeqLock示例) - Peter Cordes