В чем разница между этим «атомарным» кодом Rust и его «неатомным» аналогом? - PullRequest
0 голосов
/ 03 декабря 2018

Я довольно новичок в Rust.Я получил высшее образование по специальности «Компьютерная инженерия» 4 года назад, и я не забываю обсуждать (и понимать) атомарные операции в своем курсе по операционным системам.Однако после выпуска я работаю в основном на языках высокого уровня, где мне не нужно было заботиться о вещах низкого уровня, таких как атомарность.Теперь, когда я вхожу в Rust, я пытаюсь вспомнить, как работает большая часть этого материала.

В настоящее время я пытаюсь понять исходный код библиотеки hibitset ,в частности atomic.rs .

Этот модуль задает тип AtomicBitSet, который соответствует типу BitSet из lib.rs, но с использованием атомарных значений и операций.Насколько я понимаю, «атомарная операция» - это операция, которая гарантированно не будет прервана другим потоком;любой «load» или «store» с одним и тем же значением должен будет дождаться завершения операции, прежде чем продолжить.Из этого определения следует, что «атомарное значение» - это значение, операции которого являются полностью атомарными.AtomicBitSet использует AtomicUsize, который является оболочкой usize, где все методы полностью атомарны.Однако AtomicBitSet определяет несколько операций, которые, по-видимому, не являются атомарными (add и remove), и есть одна атомарная операция: add_atomic.Глядя на add против add_atomic, я не могу точно сказать, в чем разница.

Вот add (дословно):

/// Adds `id` to the `BitSet`. Returns `true` if the value was
/// already in the set.
#[inline]
pub fn add(&mut self, id: Index) -> bool {
    use std::sync::atomic::Ordering::Relaxed;

    let (_, p1, p2) = offsets(id);
    if self.layer1[p1].add(id) {
        return true;
    }

    self.layer2[p2].store(self.layer2[p2].load(Relaxed) | id.mask(SHIFT2), Relaxed);
    self.layer3
        .store(self.layer3.load(Relaxed) | id.mask(SHIFT3), Relaxed);
    false
}

Этот метод вызывает load() и store() напрямую.Я предполагаю, что тот факт, что он использует Ordering::Relaxed, делает этот метод неатомарным, потому что другой поток, выполняющий то же самое с другим индексом, мог бы засорять эту операцию.

Вот add_atomic (дословно):

/// Adds `id` to the `AtomicBitSet`. Returns `true` if the value was
/// already in the set.
///
/// Because we cannot safely extend an AtomicBitSet without unique ownership
/// this will panic if the Index is out of range.
#[inline]
pub fn add_atomic(&self, id: Index) -> bool {
    let (_, p1, p2) = offsets(id);

    // While it is tempting to check of the bit was set and exit here if it
    // was, this can result in a data race. If this thread and another
    // thread both set the same bit it is possible for the second thread
    // to exit before l3 was set. Resulting in the iterator to be in an
    // incorrect state. The window is small, but it exists.
    let set = self.layer1[p1].add(id);
    self.layer2[p2].fetch_or(id.mask(SHIFT2), Ordering::Relaxed);
    self.layer3.fetch_or(id.mask(SHIFT3), Ordering::Relaxed);
    set
}

Этот метод использует fetch_or вместо прямого вызова load и store, что, как я предполагаю, и делает этот метод атомарным.

Но почемуиспользование Ordering::Relaxed все еще позволяет это считать атомарным?Я понимаю, что отдельные операции "или" являются атомарными, но полный метод может быть запущен одновременно с другим потоком.Разве это не повлияло бы?

Кроме того, почему такой тип предоставляет неатомарные методы?Это только для производительности?Это кажется мне запутанным.Если бы я выбрал AtomicBitSet вместо BitSet, потому что он будет использоваться более чем одним потоком, я бы, вероятно, хотел бы использовать только атомарные операции над ним.Если бы я не сделал, я бы не использовал его.Правильно?

Я бы тоже хотел получить пояснение к комментарию внутри add_atomic.Как-то это не имеет смысла для меня.Разве неатомарная версия все еще должна заботиться об этом?Кажется, что эти два метода эффективно делают одно и то же, просто с разными уровнями атомарности.

Мне бы очень хотелось, чтобы какая-то помощь обернула голову вокруг атома.Я думаю Я понимаю порядок после прочтения этого и этого , но оба все еще используют понятия, которые я не понимаю.Когда они говорят о том, что один поток «видит» что-то из другого, что это значит?Когда говорится, что последовательно согласованные операции имеют одинаковый порядок «во всех потоках», что это вообще означает?Меняет ли процессор по-разному порядок команд для разных потоков?

1 Ответ

0 голосов
/ 03 декабря 2018

В неатомарном случае эта строка:

self.layer2[p2].store(self.layer2[p2].load(Relaxed) | id.mask(SHIFT2), Relaxed);

более или менее эквивалентна:

let tmp1 = self.layer2[p2];
let tmp2 = tmp1 | id.mask(SHIFT2);
self.layer2[p2] = tmp2;

, поэтому другой поток может изменить self.layer2[p2] между моментомсчитывается в tmp1 и момент tmp2 сохраняется в нем.Поэтому, если другой поток пытается установить другой бит в то же время, существует риск, что произойдет следующая последовательность:

  • поток 1 читает пустую маску,
  • поток 2 читаетпустая маска,
  • поток 1 устанавливает бит 1 маски и записывает его,
  • поток 2 устанавливает бит 2 маски и записывает его, перезаписывая значение, установленное потоком 1,
  • в конце установлен только бит 2!

То же самое относится к self.layer3.

В атомарном случае использование fetch_or гарантирует, чтоВесь цикл чтения-изменения-записи является атомарным.

В обоих случаях, поскольку порядок упорядочен, записи в layer2 и layer3 могут показаться в любом порядке, как видно из других потоков.

Комментарий внутри add_atomic позволяет избежать проблемы, когда два потока пытаются добавить один и тот же бит.Предположим, что add_atomic было написано так:

pub fn add_atomic(&self, id: Index) -> bool {
    let (_, p1, p2) = offsets(id);

    if self.layer1[p1].add(id) {
        return true;
    }

    self.layer2[p2].fetch_or(id.mask(SHIFT2), Ordering::Relaxed);
    self.layer3.fetch_or(id.mask(SHIFT3), Ordering::Relaxed);
    false
}

Тогда вы рискуете следующей последовательностью:

  • поток 1 устанавливает бит 1 в layer1 и видит, что он не был 't установлен заранее,
  • поток 2 пытается установить бит 1 в layer1 и видит, что поток 1 уже установил его, поэтому поток 2 возвращается из add_atomic,
  • поток 2 выполняет другую операциюэто требует чтения layer3, , но layer3 еще не обновлено, поэтому поток 2 получает неправильное значение!
  • поток 1 обновляет layer3, но это слишком поздно.

Вот почему случай add_atomic гарантирует, что layer2 и layer3 установлены правильно во всех потоках, даже если казалось, что бит уже был установлен заранее.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...