Почему эта гонка данных имеет некоторые согласованные инварианты, когда авторы обновляют одну из трех переменныхatomic<iC++

Программы на C++. Форум разработчиков
Ответить
Anonymous
 Почему эта гонка данных имеет некоторые согласованные инварианты, когда авторы обновляют одну из трех переменныхatomic<i

Сообщение Anonymous »

У меня есть следующая программа. Соответствующая информация:
  • Есть 3 атомарные переменные x,y,z, к которым имеют доступ все потоки.
  • 3 потока записи: каждый поток считывает все 3 значения x,y,z и обновляет ровно 1 переменную x, y или z. Значения сохраняются в значениях1, значениях2, значениях3
  • 2 потока чтения: каждый пытается прочитать значения x,y,z и сохранить их внутри значений4 и значений5
  • Каждое из значений1..5 представляет собой массив структур, написанных только одним потоком и читаемых только основным потоком после присоединитьсяк теме, которая это написала. Они записывают то, что видел каждый поток во время работы с атомарной загрузкой.
  • Между всеми потоками возникает состояние гонки, поскольку синхронизация отсутствует. Однако в каждом потоке значение обновляемой переменной единообразно (например, поток 1 всегда будет видеть значения x как 0, 1, 2, 3, ..., 9)
  • GCC 14.2, процессор x86, опция компиляции g++ -o main main.cpp -O3 -std=c++20
  • ОС — Ubuntu 22.04 и 24.04 (проверено на двух виртуальных машинах, в обоих случаях описанное ниже поведение клавиш одинаково)
  • Пример вывода (возможных вариантов много):
    t1: (0,0,0),(1,0,2),(2,0,2),(3,0,4),(4,0,5),(5,0,6),(6,0,7),(7,0,8),(8,0,9),(9,0,9)

    t2: (10,0,10),(10,1,10),(10,2,10),(10,3,10),(10,4,10),(10,5,10),(10,6,10),(10,7,10),(10,8,10),(10,9,10)

    t3: (0,0,0),(1,0,1),(2,0,2),(3,0,3),(4,0,4),(5,0,5),(6,0,6),(7,0,7),(8,0,8),(9,0,9)

    t4: (5,0,5),(8,0,8),(9,0,9),(10,0,10),(10,1,10),(10,2,10),(10,2,10),(10,3,10),(10,4,10),(10,5,10)

    t5: (10,2,10),(10,3,10),(10,4,10),(10,5,10),(10,6,10),(10,7,10),(10,7,10),(10,8,10),(10,9,10),(10,10,10)

    Другой
    (0,10,10),(1,10,10),(2,10,10),(3,10,10),(4,10,10),(5,10,10),(6,10,10),(7,10,10),(8,10,10),(9,10,10)

    (0,0,0),(0,1,0),(0,2,0),(0,3,0),(0,4,0),(0,5,0),(0,6,0),(0,7,0),(0,8,0),(0,9,0)

    (0,10,0),(0,10,1),(0,10,2),(0,10,3),(0,10,4),(0,10,5),(0,10,6),(0,10,7),(0,10,8),(0,10,9)

    (0,2,0),(0,3,0),(0,4,0),(0,5,0),(0,6,0),(0,7,0),(0,8,0),(0,9,0),(0,10,0),(0,10,0)

    (10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10),(10,10,10)
Вы можете видеть, что для каждого потока записи принадлежащая ему переменная имеет постоянное изменение от 0 до>9.
Это неопределенное поведение, поэтому технически может произойти все что угодно. (Примечание редактора: на самом деле здесь нет UB; единственные одновременно записываемые и читаемые объекты — это std::atomic x,y,z. Это состояние гонки, но не UB гонки данных из-за std::atomic. Итак, интересные вопросы: какой набор возможных результатов допускает ISO C++, и почему фактические результаты на x86 являются таким ограниченным подмножеством этих результатов? И вопрос об архитектуре ЦП ниже: как Когерентность аппаратного кэша обеспечивает некоторые гарантии C++.)
Однако я заметил две вещи:
  • Внутри каждого потока и для каждой переменной ее значение никогда не «возвращается во времени». Например, если переменная обновляется как «1-2-3-4-5-...-9-10», и если поток в первый раз загружает переменную, он видит «4», тогда он никогда не увидит 1, 2 или 3. Он может только стоять на месте (застрял при виде 4), или продолжить и застрять где-то на 5-9, или продолжить до конца (см. 10)
  • В группе из 3 потоков (скажем, поток 1->3), если последние значения x, которые они видят, равны (3,2,0), то поток 1 устанавливает x = 4, следующие возможные значения, которые видят потоки 3: (4,2,0), (4,2,1), (4,2,2), (4,2,3), (4,2,4), (4,3,0), ..., (4,4,4). Наиболее заметно то, что если поток 2 видит x == 4, вполне возможно, что поток 3 видит x == 0
Что именно в аппаратном или программном обеспечении вызывает такое поведение? Я могу лишь предположить несколько объяснений, но не уверен, верны они или нет.
  • Значения не уменьшаются, поскольку считываются из кэша. И значение каждой загрузки — это только последнее значение, записанное в кеш.
  • 2 потока могут видеть разные значения, поскольку их кеш может обновляться в разном порядке.
  • Некоторые значения отсутствуют из-за переключения контекста и выхода (например, после чтения x == 2, потока 4 std::yield и пробуждаются только после завершения потока 1 и x == 10).
#include
#include
#include
#include
std::atomic x(0),y(0),z(0);
std::atomic go(false);
unsigned const loop_count=10;
struct read_values
{
int x,y,z;
};
read_values values1[loop_count];
read_values values2[loop_count];
read_values values3[loop_count];
read_values values4[loop_count];
read_values values5[loop_count];
void increment(std::atomic* var_to_inc,read_values* values)
{
while(!go)
std::this_thread::yield();
for(unsigned i=0; istore(i+1,std::memory_order_relaxed);
std::this_thread::yield();
}
}
void read_vals(read_values* values)
{
while(!go)
std::this_thread::yield();
for(unsigned i=0; i

Подробнее здесь: https://stackoverflow.com/questions/798 ... pdating-on
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «C++»