В С++ могут ли атоматики пострадать от ложных хранилищ?
Например, предположим, что m
и n
являются атомарными и m = 5
изначально. В потоке 1,
m += 2;
В потоке 2,
n = m;
Результат: конечное значение n
должно быть 5 или 7, правильно? Но может ли это быть побочным? Может ли это ложно быть 4 или 8 или даже что-то еще?
Другими словами, позволяет ли модель памяти С++ запретить нить 1 вести себя, как если бы она это сделала?
++m;
++m;
Или, что более странно, как будто это так?
tmp = m;
m = 4;
tmp += 2;
m = tmp;
Ссылка: H.-J. Boehm and S. V. Adve, 2008, Рисунок 1. (Если вы следуете ссылке, то в разделе 1 бумаги см. Первый маркированный товар: "Неофициальные спецификации, предоставленные..." )
ВОПРОС В АЛЬТЕРНАТИВНОЙ ФОРМЕ
Один ответ (оцененный) показывает, что вышеупомянутый вопрос может быть неправильно истолкован. Если это полезно, то вот вопрос в альтернативной форме.
Предположим, что программист попытался передать нить 1, чтобы пропустить операцию:
bool a = false;
if (a) m += 2;
Сохраняет ли модель памяти С++ нить 1 во время выполнения, как будто она это сделала?
m += 2; // speculatively alter m
m -= 2; // oops, should not have altered! reverse the alteration
Я прошу, потому что Boehm и Adve, ранее связанные, похоже, объясняют, что многопоточное выполнение может
- спекулятивно изменить переменную, но затем
- позже измените переменную на ее исходное значение, когда спекулятивные изменения окажутся ненужными.
КОД КОМПАЛЬНОГО ОБРАЗЦА
Вот какой код вы действительно можете скомпилировать, если хотите.
#include <iostream>
#include <atomic>
#include <thread>
// For the orignial question, do_alter = true.
// For the question in alternate form, do_alter = false.
constexpr bool do_alter = true;
void f1(std::atomic_int *const p, const bool do_alter_)
{
if (do_alter_) p->fetch_add(2, std::memory_order_relaxed);
}
void f2(const std::atomic_int *const p, std::atomic_int *const q)
{
q->store(
p->load(std::memory_order_relaxed),
std::memory_order_relaxed
);
}
int main()
{
std::atomic_int m(5);
std::atomic_int n(0);
std::thread t1(f1, &m, do_alter);
std::thread t2(f2, &m, &n);
t2.join();
t1.join();
std::cout << n << "\n";
return 0;
}
Этот код всегда печатает 5
или 7
при запуске. (На самом деле, насколько я могу судить, он всегда печатает 7
, когда я его запускаю.) Однако я ничего не вижу в семантике, которая помешала бы ей печатать 6
, 4
или 8
.
Отличный Cppreference.com утверждает, "Атомные объекты свободны от гонок данных", что приятно, но в таком контексте, как этот, что это значит?
Несомненно, все это означает, что я не очень хорошо разбираюсь в семантике. Любое освещение, которое вы можете пролить на вопрос, будет оценено.
ОТВЕТЫ
@Christophe, @ZalmanStern и @BenVoigt каждый освещает вопрос с мастерством. Их ответы скорее сотрудничают, чем конкурируют. На мой взгляд, читатели должны прислушаться ко всем трем ответам: сначала Кристофе; @ZalmanStern второй; и @BenVoigt для подведения итогов.