Является ли потенциальная гонка данных гонкой данных? - PullRequest
0 голосов
/ 17 января 2020

Я пытаюсь понять, где проходит граница между гонкой данных и гонкой без даты, и каковы последствия неопределенного поведения.

Рассмотрим этот пример:

#include <chrono>
#include <thread>
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <functional>

void write(int delay, int& value, int target) {
    std::this_thread::sleep_for(std::chrono::milliseconds(delay));
    value = target;
}

int main() {
    int x;
    std::srand(std::time(nullptr));
    auto t1 = std::thread(write, rand()%100, std::ref(x), 42);
    auto t2 = std::thread(write, rand()%100, std::ref(x), 24);
    t1.join();
    t2.join();
    std::cout << x;
}

Делает ли это У кода есть данные гонки всегда или только иногда? Поведение вышеуказанного кода не определено в соответствии со стандартом всегда или только иногда (в зависимости от результата rand())?

PS: Конечно, я не могу знать, будет ли вывод 42 или 24, но при наличии неопределенного поведения я бы даже не ожидал одно из двух с уверенностью, это может быть 123 или "your cat ate my fish".

PPS: меня не волнует случайность высокого качества следовательно, rand() подходит для этого примера.

Ответы [ 3 ]

7 голосов
/ 17 января 2020

Этот код всегда имеет гонку данных. Не существует порядка , который происходит до между двумя записями, поэтому существует гонка данных.

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

2 голосов
/ 17 января 2020

В архитектурах, которые могут хранить значение до int в одном цикле шины, вы получите либо 42, либо 24 в качестве выходных данных, но никогда 123 или любое другое значение. Однако теоретически вы могли бы иметь многоядерный процессор, где int больше собственной ширины данных, что требует нескольких хранилищ, и в этом случае хранилища двух потоков могут чередоваться. На самом деле это происходит на практике на 32-битных процессорах при попытке сохранить в uint64_t.

1 голос
/ 17 января 2020

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

Например, учитывая int x, y, *p;, рассмотрите следующий фрагмент кода:

x = 0x0123;
y = *p;
... maybe some computations here that use up CPU registers
x = 0x0124;

Реализация может заметить, что между хранилищами 0x0123 и 0x0124 значение x читается, но не записывается, и, таким образом, можно изменить последнюю запись с последовательности, подобной mov ax,0124h / mov _x,ax или mov word [_x],0124h (на 8088, любая последовательность будет составлять шесть байтов плюс две операции с памятью) на inc byte _x (четыре байта плюс две операции с памятью). Однако такой код может плохо работать, если существует промежуточная запись какого-либо другого значения, например 0x00FF. Нисходящий код может рассматривать любое ненулевое значение x как одинаково приемлемое, и исходный код никогда не запрашивал записи какого-либо значения, чей нижний байт равен нулю, но если внешняя запись хранит 0x00FF непосредственно перед инструкцией inc byte _x, x можно оставить равным нулю, что довольно неожиданно.

Во многих случаях стоимость воздержания от такой оптимизации будет меньше, чем стоимость включения барьеров памяти, достаточных для предотвращения скачек данных, но, к сожалению, нет хороший способ указать такую ​​семантику, если не заменить x на «atomi c int», что может потребовать дополнительной памяти, и изменить все обращения к x, чтобы явно использовать расслабленную семантику.

...