Я потратил несколько часов на изучения темы порядков памяти, и у меня в голове остались некоторые противоречия. Одно из них о Acquire/Release порядках памяти. Сейчас у меня в голове есть такие два утверждения о них:
1 — Никакие операции после Acquire не могут быть переставлены выше это операции, когда как никакие операции до Release не могут быть переставлены после неё;
2 — Использованные на одной области памяти порядки Acquire/Release позволяют операциям после Acquire видеть все побочные операции (side affects) до Release.
А также есть наблюдение и слова других людей, что Acquire/Release работают только в паре. И тут у меня немного ломается представление модели в голове. Пускай я реализую SPSC кольцевую очередь. И у меня есть два метода: maybe_push и consumer_pop_many.
Код на Rust
pub unsafe fn consumer_pop_many(&self, dst: &mut [MaybeUninit<T>]) -> usize {
let head = unsafe { self.head.unsync_load() }; // only consumer can change head
let tail = self.tail.load(Acquire); // (1)
let available = Self::len(head, tail);
let n = dst.len().min(available);
if n == 0 {
return 0;
}
let dst_ptr = dst.as_mut_ptr();
let head_idx = head as usize % CAPACITY;
let right = CAPACITY - head_idx;
// copy data
self.head.store(head.wrapping_add(n as LongNumber), Release); // (2)
n
}
pub unsafe fn producer_maybe_push(&self, value: T) -> Result<(), T> {
let tail = unsafe { self.tail.unsync_load() }; // only the producer can change tail
let head = self.head.load(Relaxed); // (3)
if unlikely(Self::len(head, tail) == CAPACITY) {
return Err(value);
}
debug_assert!(Self::len(head, tail) < CAPACITY);
unsafe {
self.buffer_mut_thin_ptr()
.add(tail as usize % CAPACITY)
.write(MaybeUninit::new(value));
self.tail.store(tail.wrapping_add(1), Release); // (4)
};
Ok(())
}
Код на C++
bool consumer_pop_many(T* dst, size_t& count) {
Index head_val = head.unsync_load(); // only consumer can change head
Index tail_val = tail.load(std::memory_order_acquire); // (1)
size_t available = len(head_val, tail_val);
size_t n = std::min(count, available);
if (n == 0) {
count = 0;
return false;
}
size_t head_idx = static_cast<size_t>(head_val % CAPACITY);
size_t right = CAPACITY - head_idx;
// copy data
head.store(head_val + n, std::memory_order_release); // (2)
count = n;
return true;
}
bool producer_maybe_push(T&& value) {
Index tail_val = tail.unsync_load(); // only the producer can change tail
Index head_val = head.load(std::memory_order_relaxed); // (3)
if (len(head_val, tail_val) == CAPACITY) {
return false;
}
size_t idx = static_cast<size_t>(tail_val % CAPACITY);
new (&buffer[idx]) T(std::move(value));
tail.store(tail_val + 1, std::memory_order_release); // (4)
return true;
}
В 1 я использую Acquire, чтобы увидеть запись от 4 (побочный эффект). Все остальные операции зависят от этой загрузки и не будут переупорядочены до неё в любом случае, но Acquire даёт и такую гарантию тоже.
В 2 я использую Release, чтобы операция перекопирования произошла точно раньше передвижения головы.
В 4 я использую Release, чтобы запись произошла точно раньше и чтобы 1 видел эту запись.
Но вот могу ли я использовать Relaxed в 3? Мне всё равно на перемещение инструкций в этом методе, так как tail я могу загрузить и позднее, а все остальные операции зависят от результата этой. Но что насчёт побочных эффектов? Чтение может быть побочным эффектом? Оно упорядочено Release в consumer_pop_many, но может ли возникнуть ситуация, когда первый поток прочитал и сдвинул голову, а второй поток прочитал голову, но сделал это до чтения первым потоком? Выглядит, будто это невозможно, но примеры с SeqCst показывают интересные ситуации, так что этот вопрос я не могу выбросить у себя из головы.