Я пытаюсь найти свои ноги в программировании без блокировки. Прочитав различные объяснения семантики упорядочения памяти, я хотел бы прояснить, что может произойти в результате возможного переупорядочения. Насколько я понял, инструкции могут быть переупорядочены компилятором (из-за оптимизации при компиляции программы) и ЦП (во время выполнения?).Свободное программирование: семантика порядка переупорядочения и памяти
Для расслабленной семантики cpp reference обеспечивает следующий пример:
// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B
// Thread 2:
r2 = x.load(memory_order_relaxed); // C
y.store(42, memory_order_relaxed); // D
Говорит, что при х и у, первоначально нуля код разрешено производить r1 r2 == == 42, потому что, хотя секвенируют -be before B в потоке 1 и C секвенируется до D в потоке 2, ничто не мешает D появляться до того, как A в порядке модификации y, а B появится до C в порядке модификации x. Как это могло случиться? Означает ли это, что C и D переупорядочиваются, поэтому порядок выполнения будет DABC? Разрешено ли переупорядочивать A и B?
Для семантики приобретает релиз есть следующий пример код:
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
мне интересно, что если бы мы использовали непринужденный заказ памяти вместо приобретают? Я думаю, значение data
можно было прочитать до p2 = ptr.load(std::memory_order_relaxed)
, но как насчет p2
?
И наконец, почему в этом случае хорошо использовать расслабленный порядок памяти?
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
// put the current value of head into new_node->next
new_node->next = head.load(std::memory_order_relaxed);
// now make new_node the new head, but if the head
// is no longer what's stored in new_node->next
// (some other thread must have inserted a node just now)
// then put that new head into new_node->next and try again
while(!head.compare_exchange_weak(new_node->next, new_node,
std::memory_order_release,
std::memory_order_relaxed))
; // the body of the loop is empty
}
};
Я имею в виду как head.load(std::memory_order_relaxed)
и head.compare_exchange_weak(new_node->next, new_node, std::memory_order_release, std::memory_order_relaxed)
.
Чтобы суммировать все вышесказанное, мой вопрос по существу, когда мне нужно заботиться о возможном переупорядочении, а когда нет?
Обратите внимание, что # 2 будет иметь неопределенное поведение в соответствии со стандартом C++, если 'int data' был прочитан без синхронизации с гарантией, которая останавливает поток чтения от просмотра, пока он все еще записывается. Это будет гонка данных типа UB, так как это не std :: атомный тип. 'std :: atomic' использование 'memory_order_relaxed' позволит избежать UB' data' и просто оставит вас с действительной (но менее полезной) программой с ошибкой гонки данных о садовом разнообразии. Но сравнение строк после разыменования 'ptr' будет по-прежнему представлять собой гонку данных, потому что' std :: string' не является контейнером с атомарным/свободным доступом. –
@PeterCordes Я действительно не понимаю, почему сравнение строк будет представлять собой гонку данных. У потребителя мы ждем, пока инициализируется 'p2'. Затем мы сравниваем строку. Из-за правила «не нарушать однопоточный код» этот порядок должен быть сохранен. Как только мы получили что-то в 'p2', мы гарантированно увидим заостренные данные (как вы упомянули, на всех процессорах, кроме Alpha). Данные должны быть как минимум новыми, как указатель, поэтому, если указатель инициализирован, данные также должны быть инициализированы.Или я неправильно понял вас? – mentalmushroom
Я имел в виду, что это гонка данных, если писатель или читатель использует 'memory_order_relaxed', потому что любой из них позволит считывать указатель как не-NULL, пока данные в классе' std :: string' все еще записываются. (Обратите внимание, что компиляция для не-альфа-процессора не имеет отношения к UB или нет. Точно так же, как подписанное целочисленное переполнение является UB даже при компиляции для машины дополнений 2, где она имеет очень хорошо определенную семантику.) –