2016-10-19 7 views
1

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

Для расслабленной семантики 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).

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

+0

Обратите внимание, что # 2 будет иметь неопределенное поведение в соответствии со стандартом C++, если 'int data' был прочитан без синхронизации с гарантией, которая останавливает поток чтения от просмотра, пока он все еще записывается. Это будет гонка данных типа UB, так как это не std :: атомный тип. 'std :: atomic ' использование 'memory_order_relaxed' позволит избежать UB' data' и просто оставит вас с действительной (но менее полезной) программой с ошибкой гонки данных о садовом разнообразии. Но сравнение строк после разыменования 'ptr' будет по-прежнему представлять собой гонку данных, потому что' std :: string' не является контейнером с атомарным/свободным доступом. –

+0

@PeterCordes Я действительно не понимаю, почему сравнение строк будет представлять собой гонку данных. У потребителя мы ждем, пока инициализируется 'p2'. Затем мы сравниваем строку. Из-за правила «не нарушать однопоточный код» этот порядок должен быть сохранен. Как только мы получили что-то в 'p2', мы гарантированно увидим заостренные данные (как вы упомянули, на всех процессорах, кроме Alpha). Данные должны быть как минимум новыми, как указатель, поэтому, если указатель инициализирован, данные также должны быть инициализированы.Или я неправильно понял вас? – mentalmushroom

+0

Я имел в виду, что это гонка данных, если писатель или читатель использует 'memory_order_relaxed', потому что любой из них позволит считывать указатель как не-NULL, пока данные в классе' std :: string' все еще записываются. (Обратите внимание, что компиляция для не-альфа-процессора не имеет отношения к UB или нет. Точно так же, как подписанное целочисленное переполнение является UB даже при компиляции для машины дополнений 2, где она имеет очень хорошо определенную семантику.) –

ответ

2

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

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

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

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

+0

Не могли бы вы подробнее остановиться на пункте № 2: в каком сценарии * p2 было бы бессмысленным? Что касается пункта № 3, я понимаю, что он использует выпуск для публикации, но мой вопрос касался больше нагрузок (начальная загрузка на «new_node-> next» и нагрузка в случае сбоя 'compare_exchange_weak'). Почему нам не нужен порядок? – mentalmushroom

+0

@mentalmushroom '* p2' не был опубликован для расслабленной темы читателя. Он был опубликован для тех, кто действительно приобретает. В № 3 нет ничего неатомного, который читается из памяти, поэтому ничего не нужно приобретать. – Cubbi

+0

Если он не был опубликован для расслабленного читателя, почему 'ptr.load()' возвращает ненулевое значение? В # 3 'new_node' и' new_node-> next' неатомные, не так ли? – mentalmushroom