Записывает ли одно и то же значение в одну и ту же ячейку памяти гонку данных? - PullRequest
14 голосов
/ 29 ноября 2011

Рассмотрим следующий код, который записывает одно и то же значение в одну и ту же ячейку памяти из нескольких потоков:

void f(int* buf, int n, int* p) {
    for(int i = 0; i < n; i++)
        buf[i] = i;
    *p = buf[n/2];
}

void g(int* buf, int n) {
    int x1, x2;
    thread t1(f, buf, n, &x1);
    thread t2(f, buf, n, &x2);
    t1.join();
    t2.join();
    assert(x1 == x2);
}

Хотя это интересно, меня меньше беспокоит вопрос о том, что дает стандарт, так как я предполагаюэто не дает ничего.Меня волнует, как поведет себя приведенный выше код на реальном многопроцессорном оборудовании.Будет ли assert всегда проходить или есть вероятность возникновения состояния гонки, проблем с синхронизацией кэша и т. Д.?

Ответы [ 3 ]

8 голосов
/ 29 ноября 2011

Гонка есть, но в вашем примере оба потока будут записывать одинаковые значения по одинаковым адресам.Поскольку вы не делаете никаких операций чтения-изменения-записи, а просто записываете заранее заданные числа, в большинстве случаев это будет безопасно.Написание int будет атомарной инструкцией в большинстве систем.Исключением будет, если вы запустите этот код на 8-битном микропроцессоре, который использует последовательность инструкций для хранения int.В этом случае это также может все еще работать, но зависит от реализации библиотечного кода, который делает многобайтовое хранилище.

6 голосов
/ 29 ноября 2011

Модели памяти с учетом многопоточности, когда эффекты записи, сделанные одним потоком, наблюдаются другим потоком. В коде, который вы разместили, оба потока записывают одинаковые значения в одну и ту же ячейку памяти, поэтому не имеет значения, какой поток записывает запись buf[n/2], подойдет любой.

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

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

2 голосов
/ 02 декабря 2011

ОК, ключевой момент здесь, как сказал @Maxim, когерентность кэша. В машине, использующей когерентность кэша, это действительно невозможно.

Однако, может работать неправильно на машине без когерентности кэша. Я не знаю конкретной архитектуры, и хотя они почти вымерли из-за естественного отбора, насколько я знаю, некоторые еще остались. (Если вы знаете пример, пожалуйста, прокомментируйте.)

Вот таблица, которая представляет выполнение двух потоков, заполняющих нулевую область в памяти единицами. Для краткости этот пример уменьшен в 32 раза, то есть каждая цифра здесь представляет рассматриваемый 4-байтовый тип int. Размер строки кэша составляет 4 дюйма == 4 цифры. Строки, помеченные как «очищенные», представляют собой точки, в которых кэш-память на кристалле сбрасывается в основную память. В действительности это недетерминировано, как это может случиться в любое время, например, из-за упреждающего переключения задач.

Core 1 cache              Memory                    Core 2 cache
------------------------------------------------------------------------------
                          0000
0000 (load cache)         0000
1000 (set 1st bit)        0000
1100 (set 2nd bit)        0000                      0000 (load cache)
**** (flush)              1100
                          1100                      1000 (set 1st bit)
                          1000                      **** (flush)
                          1000                      1000 (load cache)
                          1000                      1100 (set 2nd bit)
1000 (load cache)         1000                      1110 (set 3rd bit)
1010 (set 3rd bit)        1000                      1111 (set 4th bit)
1011 (set 4th bit)        1111                      **** (flush)
**** (flush)              1011

Итак, в итоге мы получили неверный результат.

Я еще раз подчеркиваю, что этот контрпример действителен только для машин, не связанных с кэшем .

...