Программирование без блокировки: переупорядочение и семантика порядка памяти

Я пытаюсь найти свои ноги в программировании без блокировки. Прочитав различные объяснения семантики упорядочения памяти, я хотел бы выяснить, что может произойти переупорядочение. Насколько я понял, инструкции могут быть переупорядочены компилятором (из-за оптимизации во время компиляции программы) и ЦП (во время выполнения?).

Для упрощенной семантики ссылка cpp предоставляет следующий пример:

// 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

Говорят, что при x и y, изначально равных нулю, коду разрешено создавать r1 == r2 == 42, потому что, хотя A секвенируется до B в потоке 1, а C чередуется до D в потоке 2, ничто не мешает D появиться до A в порядке модификации y, и B от появления перед C в порядке изменения x. Как это могло случиться? Означает ли это, что C и D переупорядочиваются, поэтому порядок выполнения будет DABC? Можно ли изменить порядок A и B?

Для семантики acqu-release есть следующий пример кода:

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),

Подводя итог всему вышесказанному, мой вопрос в основном, когда мне нужно заботиться о возможном переупорядочении, а когда нет?

1 ответ

Для #1 компилятор может выдать хранилище для y до загрузки из x (нет никаких зависимостей), и даже если это не так, загрузка из x может быть отложена на уровне процессора / памяти.

Для #2 p2 будет отличным от нуля, но ни *p2, ни данные не обязательно будут иметь значащее значение.

Для #3 существует только один акт публикации неатомарных магазинов, созданных этой веткой, и это релиз

Вы должны всегда заботиться о переупорядочении, или, лучше, не принимать никакого порядка: ни C++, ни аппаратное обеспечение не выполняют код сверху вниз, они только уважают зависимости.

Другие вопросы по тегам