Стандарты C11 и C ++ 11 определяют, что одновременное неатомарное чтение и запись в одну и ту же ячейку памяти является гонкой данных, которая приводит к UB, поэтому такая программа может делать практически все, что угодно. Хорошо понял. Я хочу понять причину этого слишком (для меня сегодня) строгих требований.
Я играю с еще одним механизмом IPC, использующим этот плохой и быстрый доступ к памяти. Я не буду беспокоить вас всеми подробностями, позвольте мне показать упрощенную версию:
template <typename T>
class sval {
static_assert(std::is_trivially_copy_assignable<T>::value, "");
static_assert(std::is_trivially_destructible<T>::value, "");
static constexpr unsigned align = 64;
unsigned last;
alignas(align) std::atomic<unsigned> serial;
struct alignas(align) {
T value;
} buf[2];
public:
sval(): last(0), serial(last) {}
sval(sval &) = delete;
void operator =(sval &) = delete;
void write(const T &a) {
++last;
buf[last & 1].value = a;
serial.store(last, std::memory_order_release);
}
class reader {
const sval &sv;
unsigned last;
public:
reader(const sval &sv): sv(sv), last(sv.serial.load(std::memory_order_relaxed)) {}
bool read(T &a) {
unsigned serial = sv.serial.load(std::memory_order_acquire);
if (serial == last) {
return false;
}
for (;;) {
a = sv.buf[serial & 1].value;
unsigned check = sv.serial.load(std::memory_order_seq_cst);
if (check == serial) {
last = check;
return true;
}
serial = check;
}
}
};
};
Это общая ценность для одного автора и нескольких читателей. Он содержит два буфера внизу, buf[serial & 1]
является «запечатанным», а другой может обновляться. Логика писателя очень проста (и это главная особенность), на которую не влияют присутствие и активность читателей. Но читатель должен сделать больше, чтобы гарантировать согласованность извлеченных данных (и это мой главный вопрос здесь):
serial
числоbuf[serial & 1]
serial
еще раз и повторите попытку, если он был измененТак что он может читать данные мусора посередине, но потом проверяет. Возможно ли, что что-то плохое вытечет из read()
Внутренности? Если да, то каковы точные причины с аппаратной стороны или где-то еще?
Ниже приведен пример приложения. Я протестировал и это приложение, и мою оригинальную более сложную идею на x86 и ARM. Но ни разнообразие тестов, ни покрытие платформ не дают мне уверенности в своих идеях.
int main() {
const int N = 10000000;
sval<int> sv;
std::thread threads[2];
for (auto &t : threads) {
t = std::thread([&sv] {
sval<int>::reader r(sv);
int n = 0;
for (int i = 0, a; i < N; i = a) {
while (!r.read(a)) {
}
assert(a > i);
++n;
}
std::printf("%d\n", n);
});
}
int dummy[24];
for (int i = 1; i <= N; ++i) {
std::rotate(std::begin(dummy), dummy + 11, std::end(dummy));
sv.write(i);
}
for (auto &t : threads) {
t.join();
}
}
Задача ещё не решена.