Какие из этих реализаций seqlock являются правильными? - PullRequest
6 голосов
/ 03 июня 2019

Я изучаю реализацию 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 с семантикой получения в Kernel.

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, но вместо этого здесь используется энергозависимое чтение.

Могут ли 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: побайтный атомный memcpy

В этом документе утверждается, что:

В общем случае существуют веские семантические причины требовать, чтобы все обращения к данным внутри такой секвенциальной «критической секции» были атомарными. Если мы читаем указатель p как часть чтения данных, а затем читаем также * p, код внутри критической секции может считываться с неверного адреса, если при чтении p произошло половинное обновление значения указателя. В таких случаях, вероятно, нет способа избежать чтения указателя с обычной атомарной нагрузкой, и это именно то, что нужно.

Однако во многих случаях, особенно в случае нескольких процессов, данные seqlock состоят из одного тривиально копируемого объекта, а «критическая секция» seqlock состоит из простой операции копирования. При нормальных обстоятельствах это можно было бы написать с использованием memcpy. Но это здесь недопустимо, поскольку memcpy не генерирует атомарный доступ и (в любом случае, согласно нашей спецификации) восприимчив к гонкам данных.

В настоящее время для правильного написания такого кода нам нужно в основном разбить такие данные на множество небольших безобъектных атомарных подобъектов и копировать их по частям за раз. Обработка данных как одного большого атомарного объекта отрицательно скажется на цели секвлока, поскольку операция атомарного копирования получит обычную блокировку. Наше предложение по существу добавляет удобную библиотечную функцию для автоматизации этого разложения на маленькие объекты.

Мой вопрос

  1. Какие из приведенных выше реализаций верны? Какие из них правильные, но неэффективные?
  2. Может ли volatile_read быть переупорядочен до чтения / чтения seqlock?
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...