25

我正在研究Seqlock的实现。然而,我发现的所有来源都以不同的方式实现它们。

Linux内核

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 (锈)

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,而是在这里使用易失性读取。

Seqlocks 可以与编程语言记忆模型相处吗?(变体 3)

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中的参数:逐字节原子内存

该论文声称:

在一般情况下,有充分的语义理由要求此类 seqlock “临界区”内的所有数据访问必须是原子的。如果我们读取指针 p 作为读取数据的一部分,然后也读取 *p,如果读取 p 碰巧看到一半更新的指针值,则临界区中的代码可能会从错误地址读取。在这种情况下,可能无法避免使用传统的原子负载读取指针,而这正是我们所需要的。

然而,在许多情况下,特别是在多进程情况下,seqlock 数据由一个简单的可复制对象组成,而 seqlock “临界区”由一个简单的复制操作组成。在正常情况下,这可能是使用 memcpy 编写的。但这在这里是不可接受的,因为 memcpy 不会生成原子访问,并且(无论如何根据我们的规范)容易受到数据竞争的影响。

目前要正确编写这样的代码,我们基本上需要将这样的数据分解成许多小的无锁原子子对象,并一次复制一份。将数据视为单个大型原子对象会破坏 seqlock 的目的,因为原子复制操作将获取常规锁。我们的提议本质上添加了一个方便的库工具来自动分解成小对象。

我的问题

  1. 以上哪些实现是正确的?哪些是正确但低效的?
  2. 可以在volatile_readseqlock的获取读取之前重新排序吗?
4

1 回答 1

1

您在 Linux 上的 qoutes 似乎是错误的。

根据https://www.kernel.org/doc/html/latest/locking/seqlock.html读取过程是:

Read path:

do {
        seq = read_seqcount_begin(&foo_seqcount);

        /* ... [[read-side critical section]] ... */

} while (read_seqcount_retry(&foo_seqcount, seq));

如果您查看问题中发布的 github 链接,您会发现包含几乎相同过程的评论。

您似乎只是在研究读取过程的一部分。链接的文件实现了实现读取器和写入器所需的内容,但不是他们自己的读取器/写入器。

还要注意文件顶部的这条评论:

* The seqlock seqcount_t interface does not prescribe a precise sequence of
* read begin/retry/end. For readers, typically there is a call to
* read_seqcount_begin() and read_seqcount_retry(), however, there are more
* esoteric cases which do not follow this pattern.
于 2021-05-05T16:27:13.583 回答