std :: memory_order и порядок команд, уточнение - PullRequest
2 голосов
/ 08 января 2020

Это дополнительный вопрос к этому .

Я хочу точно определить смысл порядка команд и то, как на него влияют std::memory_order_acquire, std::memory_order_release et c ...

В вопросе, который я связал, уже предоставлены некоторые детали, но я чувствовал, что предоставленный ответ на самом деле не о порядке (который был больше, чем я искал), а скорее немного мотивируя, почему это необходимо, et c.

Я приведу тот же пример, который я буду использовать в качестве ссылки

#include <thread>
#include <atomic>
#include <cassert>
#include <string>

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
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}

В двух словах, я хочу понять, что именно происходит с порядком команд в обеих строках

ptr.store(p, std::memory_order_release);

и

while (!(p2 = ptr.load(std::memory_order_acquire)))

Сосредоточение на первом согласно документации

... нет чтения или записи в текущем потоке могут быть переупорядочены после этого хранилища ...

Я смотрел несколько выступлений, чтобы понять эту проблему заказа, я понимаю, почему это важно сейчас. То, что я пока не могу понять, как компилятор переводит спецификацию порядка, я думаю, что пример, приведенный в документации, также не особенно полезен, потому что после операции сохранения в потоке, выполняющем producer, нет другой инструкции, следовательно, ничего будет переупорядочен в любом случае. Однако также возможно, что я неправильно понимаю, возможно, они означают, что эквивалентная сборка

std::string* p  = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);

будет такой, что первые две переведенные строки никогда не будут перемещены после хранилища atomi c? Аналогично в потоке, работающем с производителем, возможно ли, что ни одно из утверждений (или эквивалентной сборки) никогда не будет перемещено до загрузки атома c? Предположим, у меня была третья инструкция после магазина, что будет с этой инструкцией, которая будет уже после загрузки atomi c?

Я также попытался скомпилировать такой код, чтобы сохранить код промежуточной сборки с флаг -S, но он довольно большой, и я не могу понять.

Опять же, чтобы уточнить, этот вопрос о том, как упорядочение, а не о том, почему эти механизмы полезны или необходимы.

Ответы [ 4 ]

1 голос
/ 03 февраля 2020

Я знаю, что когда дело доходит до упорядочивания памяти, люди обычно пытаются спорить, можно ли и как переупорядочить операции, но, на мой взгляд, это неправильный подход! Стандарт C ++ не устанавливает, каким образом инструкции могут быть переупорядочены, но вместо этого определяет отношение «происходит до» , которое само основано на отношениях «перед последовательностью», «синхронизировать с» и «между потоками происходит до».

Приемная загрузка, которая считывает значение из релиза магазина , синхронизирует с , который получает загрузку, устанавливая, таким образом, отношение до появления. Из-за транзитивности отношения «происходит до» операции, которые «секвенируются до» релиза магазина, также «происходят до» получения загрузки. Любые аргументы о правильности реализации, использующей атомарность, всегда должны опираться на отношение «происходит до». Если и как инструкции могут быть переупорядочены, это просто результат применения правил для отношения «происходит до».

Для более подробного объяснения модели памяти C ++ Вы можете взглянуть на Модели памяти для программистов на C / C ++ .

0 голосов
/ 12 января 2020

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

Если бы они были, они все равно могли быть выполнены заранее. Как это повредит?

Единственное, что производитель должен гарантировать, это то, что «производство» в памяти полностью записано до того, как установлен флаг; в противном случае потребитель ничего не мог бы сделать, чтобы избежать чтения неинициализированной памяти (или старого значения объекта).

Установка слишком поздно опубликованного опубликованного объекта была бы катастрофой c. Но как начать настройку другого опубликованного объекта (скажем, второго) «слишком рано», проблема?

Как вы вообще узнаете, что продюсер делает слишком рано? Единственное, что вы разрешено делать, это проверять флаг, и только после того, как флаг установлен, вы можете наблюдать за опубликованным объектом.

Поэтому, если что-либо будет переупорядочено до изменения флага, вы не сможете его увидеть.

Но ничего не видно в выводе сборки G CC на x86-64:

producer():
        sub     rsp, 8
        mov     edi, 32
        call    operator new(unsigned long)
        mov     DWORD PTR data[rip], 42
        lea     rdx, [rax+16]
        mov     DWORD PTR [rax+16], 1819043144
        mov     QWORD PTR [rax], rdx
        mov     BYTE PTR [rax+20], 111
        mov     QWORD PTR [rax+8], 5
        mov     BYTE PTR [rax+21], 0
        mov     QWORD PTR ptr[abi:cxx11][rip], rax
        add     rsp, 8
        ret

(Если вам интересно, ptr[abi:cxx11] это оформленное имя, а не какой-то прикольный синтаксис asm, поэтому ptr[abi:cxx11][rip] означает ptr[rip].)

, который можно обобщить так:

setup stack frame
assign data
setup string object
assign ptr
remove frame and return

Так что на самом деле ничего примечательного, кроме ptr назначается последним.

Вы должны выбрать другую цель, чтобы увидеть что-то более интересное.

0 голосов
/ 14 января 2020

Может быть полезно ответить на ваш комментарий:

Я все еще чувствую, что мой вопрос не ясен, мой вопрос больше похож на следующий. Предположим (например, в производителе), что вы добавили еще несколько операторов после хранилища atomi c, например data_2 = 175 и, возможно, data_3 = 10, где data_2 и data_3 являются глобальными. Как именно влияет на повторный заказ сейчас? Я понимаю, что вы, вероятно, рассмотрели это в своем ответе, поэтому я прошу прощения, если меня раздражает

Давайте возьмемся за ваш producer()

void producer()
{
    data = 41;
    std::string* p  = new std::string("Hello");
    data = 42;
    ptr.store(p, std::memory_order_release);
}

Может consumer() найти значение 41 в «данных». Нет. Значение 42 было (логически) сохранено в данных в точке ограничителя выброса, и если бы consumer() обнаружил значение 42, хранилище 42 (по крайней мере, появилось) имело место после разделительного барьера.

ОК, теперь давайте повозимся дальше ...

void producer()
{
    data = 0xFF01;
    std::string* p  = new std::string("Hello");
    data = 0xFF02;
    ptr.store(p, std::memory_order_release);
    data = 0x0003
}

Теперь все ставки выключены. data не атоми c, и нет никакой гарантии, что consumer может найти. На большинстве архитектур реальность такова, что единственными кандидатами являются 0xFF02 или 0x0003, но, безусловно, существуют архитектуры, в которых он может найти 0xFF03 и / или 0x0002. Это может произойти в архитектуре с 8-битной шиной, где 16-битная int записывается как 2 однобайтовые операции (с любого конца).

Но в принципе сейчас просто нет гарантии, что будет храниться перед лицом такой гонки данных. Это гонка данных, потому что нет никакого контроля, чтобы гарантировать, что consumer заказан с этой дополнительной записью.

0 голосов
/ 09 января 2020

Без атома c:

std::string* ptr;
int data;

void producer()
{
    std::string* p  = new std::string("Hello");
    data = 42;
    ptr = p;
}

void consumer()
{
    std::string* p2;
    while (!(p2 = ptr))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // never fires
}

В producer компилятор может свободно перемещать назначение в данные после назначения в ptr. Поскольку ptr становится ненулевым до того, как данные установлены, это может вызвать соответствующее утверждение.

Хранилище релизов запрещает компилятору делать это.

В consumer компилятор свободен переместить утверждение в данные до того, как l oop.

Приобретение нагрузки запрещает компилятору делать это.

Не связано с упорядочением, но компилятор может опустить l oop полностью, потому что, если ptr равно нулю, когда l oop запускается, ничто не может достоверно заставить его казаться не нулевым, что приводит к бесконечному l oop, что также можно предположить, чтобы не происходить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...