Можно ли переупорядочить два последовательных хранилища memory_order_release в одном и том же потоке друг с другом? - PullRequest
1 голос
/ 17 января 2020

Можно ли переупорядочить два последовательных memory_order_release магазина в одном потоке друг с другом? Или с точки зрения того же потока или другого потока, загружающего их?

Документация на CPP ссылка гласит:

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

Так что в этом примере:

std::atomic<uint64_t> a;
std::atomic<uint64_t> b;

// ...

a.store(0xDEADBEFF, std::memory_order::memory_order_release);
b.store(0xBEEFDEAD, std::memory_order::memory_order_release);

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

Другими словами: документация говорит, что хранилище a не может быть перемещено вниз. Это также гарантирует, что b не может быть перемещено вверх?

Я пытаюсь определить, получаю ли я в другом потоке b и вижу 0xBEEFDEAD, а затем приобретаю a, если мне гарантировано см a это 0xDEADBEEF.

Ответы [ 2 ]

4 голосов
/ 17 января 2020

Понятие переупорядочения операций с памятью (например, чтения и записи) часто используется для того, чтобы сделать вопросы видимости памяти между потоками более «конкретными», поскольку переупорядочивание задач - это ежедневная проблема для любого человека, который заблокировал или разблокировал вещи делать. Но это не основа связи между потоками и видимости памяти. И, кстати, значения memory_order_x относятся к видимости, а не к «порядку». Не используйте термин «порядок в памяти»!

Release semanti c определяется обещанием для любого потока, который может видеть сохраненное значение . (Вот почему release является только свойством модификации разделяемой переменной; чтение объекта atomi c, даже с видимостью memory_order_seq_cst памяти, никогда не может быть операцией освобождения.)

Поток который видит записанное значение операции освобождения, может предполагать, что предыдущие операции «завершены». Эти операции над общими объектами, которые должны быть «завершены», - это чтение и запись, а также другие вещи, такие как создание объекта (о котором ваш источник забыл упомянуть). Операции, которые были выполнены «до» (ранее в порядке выполнения программы или даже в другом потоке, транзитивно с тем же «завершенным» свойством), могут рассматриваться как выполненные потоком, который выполняет чтение для записанного значения. (Если вы выполнили расслабленное чтение, вы можете использовать барьер получения впоследствии для получения чтения чтения semanti c.)

Важно отметить, что операции освобождения и получения являются границами и определяют взаимное исключение операций. Как и в случае мьютекса: объект atomi c используется для взаимного исключения между записанным потоком и потоком чтения.

a.store(0xDEADBEFF, std::memory_order::memory_order_release);

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

b.store(0xBEEFDEAD, std::memory_order::memory_order_release);

Эта одна операция освобождения (на b) важна: причина, почему компилятор не может «переупорядочить» материал, потому что другие потоки могут читать b (который не является частной переменной потока) и могут видеть специфицированное c значение 0xBEEFDEAD и, возможно, сделать вывод, что произошел выпуск, и использовать команду acqu semanti c до гарантирует взаимное исключение из:

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

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


Относительно цитаты:

Документация по ссылке CPP говорит:

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

Я могу легко дать хотя бы три случая, когда переупорядочение разрешено.

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

Другие преобразования, которые не могут быть выполнены с помощью вызова внешней функции, но атоми c операции известны компилятору в отличие от вызовов к отдельно скомпилированным функциям:

  • любое действие строго функционального примитива связи локального потока, будь то мьютекс или переменная atomi c, может быть переупорядочено с чем угодно никакой другой поток не может наблюдать или взаимодействовать с переменной;
  • , когда атомом c объекта A манипулируют таким образом, что компилятор может видеть все операции над ним, если сохраненное значение никогда не изменяется (оно сохраняет его первоначальное значение), то любая операция над другим объектом может быть переупорядочена, например, с хранилищем выпуска на A.

Это могут быть довольно неинтересные и глупые (кто использует мьютекс в качестве локальной переменной?) Особые случаи, но они логически существуют.

3 голосов
/ 17 января 2020
// T1
a.store(0xDEADBEFF, std::memory_order::relaxed); // #1
b.store(0xBEEFDEAD, std::memory_order::release); // #2

// T2
if (b.load(std::memory_order::acquire) == 0xBEEFDEAD) {      // #3
   assert(a.load(std::memory_order::relaxed) == 0xDEADBEEF); // #4
}

1 секвенируется до 2. 2 синхронизируется с 3, а 3 секвенируется до 4. Это означает, что 1 происходит раньше 4. По [intro.races] p18, при условии, что нет никаких других модификаций a , 4 должно принимать значение от 1, т. Е. Assert никогда не сработает.

...