Понимание `memory_order_acquire` и` memory_order_release` в C ++ 11 - PullRequest
1 голос
/ 07 января 2020

Я читаю документацию и, более конкретно,

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

memory_order_release : операция сохранения с этим порядком памяти выполняет операцию освобождения: после этого хранилища никакие операции чтения или записи в текущем потоке не могут быть переупорядочены. Все записи в текущем потоке видны в других потоках, которые получают ту же самую атомарную переменную (см. Порядок упорядочения Release-Acquire ниже), и записи, которые переносят зависимость в переменную atomi c, становятся видимыми в других потоках, которые используют тот же атом c (см. упорядочение потребления-выпуска ниже)

Эти два бита:

из memory_order_acquire

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

из memory_order_release

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

Что именно они означают?

Есть также этот пример

#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();
}

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

Ответы [ 3 ]

2 голосов
/ 07 января 2020

Работа, выполняемая потоком, не гарантируется для других потоков.

Чтобы сделать данные видимыми между потоками, необходим механизм синхронизации. Для этого можно использовать непринужденный atomic или mutex. Это называется семантикой получения-выпуска. Запись мьютекса «освобождает» все записи в память перед ним, а чтение того же мьютекса «получает» эти записи.

Здесь мы используем ptr, чтобы «освободить» проделанную работу (data = 42) в другой поток :

    data = 42;
    ptr.store(p, std::memory_order_release); // changes ptr from null to not-null

И здесь мы ждем этого и, делая это, синхронизируем ("приобретаем") работу, проделанную потоком производителя:

    while (!ptr.load(std::memory_order_acquire)) // assuming initially ptr is null
        ;
    assert(data == 42);

Обратите внимание на два различных действия:

  1. we wait между потоками (шаг синхронизации)
  2. как побочный эффект подождите , мы получаем для передачи ( релиз ) работу от поставщика к потребителю

При отсутствии (2), например, при использовании memory_order_relaxed, синхронизируется только само значение atomic. Вся остальная работа, выполненная до / после, отсутствует, например, data не обязательно будет содержать 42, и может не быть полностью сконструированного string экземпляра по адресу p (как замечено потребителем).

1 голос
/ 07 января 2020

Получение и освобождение являются барьерами памяти. Если ваша программа считывает данные после барьера получения, вы уверены, что вы будете считывать данные в соответствии с любым предыдущим выпуском любым другим потоком в отношении той же переменной atomi c. Переменные Atomi c гарантированно будут иметь абсолютный порядок (при использовании memory_order_acquire и memory_order_release, хотя предусмотрены более слабые операции) для их чтения и записи во всех потоках. Эти барьеры в действительности распространяют этот порядок на любые потоки, использующие эту переменную atomi c. Вы можете использовать атомику, чтобы указать, что что-то «закончено» или «готово», но если потребитель читает за пределами этой переменной atomi c, потребитель не может полагаться на «просмотр» правильных «версий» другой памяти, и атомы будут имеют ограниченное значение.

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

Ваш код использует объект std::string (a) созданный в producer() до присвоения ptr и (b) созданная версия этой строки (т. е. версия памяти, которую она занимает) является той, которую читает consumer(). Проще говоря, consumer() будет с нетерпением читать строку, как только увидит назначенное значение ptr, так что, черт возьми, лучше увидеть действительный и полностью построенный объект, или наступят плохие времена. В этом коде «действие» присваивания ptr - это то, как producer() «сообщает» consumer, что строка «готова». Барьер памяти существует, чтобы убедиться, что именно это видит потребитель.

И наоборот, если ptr был объявлен как обычный std::string *, компилятор мог бы решить оптимизировать p и назначить выделенный адрес непосредственно ptr, и только затем создать объект и назначить int данные. Вероятно, это катастрофа для потока consumer, который использует это назначение в качестве индикатора того, что объекты, которые готовит producer, готовы. Чтобы быть точным, если бы ptr был указателем, consumer может никогда не увидеть назначенное значение или на некоторых архитектурах прочитать частично назначенное значение, где были назначены только некоторые байты, и это указывает на расположение мусорной памяти. Однако эти аспекты связаны с тем, что атомы c не являются более широкими барьерами памяти.

1 голос
/ 07 января 2020

Если вы использовали std::memory_order_relaxed для хранилища, компилятор мог бы использовать правило "как если бы", чтобы переместить data = 42; после хранилища, а consumer мог видеть ненулевой указатель и неопределенный data .

Если вы использовали std::memory_order_relaxed для загрузки, компилятор мог бы использовать правило "как если бы", чтобы переместить assert(data == 42); до загрузки l oop.

Оба из них разрешены, потому что значение data не связано со значением ptr

Если бы вместо ptr не были атомы c, вы бы имели гонку данных и, следовательно, не определены поведение.

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