Пространства имён
Варианты
Действия

std::memory_order

Материал из cppreference.com
< cpp‎ | atomic


Определено в заголовочном файле <atomic>
enum memory_order {

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

};
(начиная с C++11)

std::memory_order (упорядочение доступа к памяти) определяет, как обычный, неатомарный доступ к памяти, упорядочивается вокруг атомарных операций. При отсутствии каких-либо ограничений, на многоядерных системах, когда множество потоков одновременно читает и пишет в несколько переменных, один поток может наблюдать изменение значений переменных в порядке, отличающемся от того, в котором другой поток записывает их. На самом деле, видимый порядок изменений может отличаться даже среди нескольких читающих потоков.

Для атомарных операций по умолчанию библиотекой предоставляется последовательно согласованное упорядочение (sequentially consistent ordering) (см обсуждение ниже). Такое поведение может повредить быстродействию, но атомарным операциям библиотеки может быть передан дополнительный std::memory_order аргумент, чтобы указать точные ограничения, помимо атомарности, которые компилятор и процессор должны обеспечить для этой операции.

Содержание

[править] Константы

Заголовочный файл <atomic>
Значение Объяснение
memory_order_relaxed Ослабленное(Relaxed) упорядочение: отсутствуют ограничения синхронизации и упорядочения, для данной операции требуется только атомарность.
memory_order_consume Операция загрузки с этим упорядочением памяти выполняет операцию поглощения (consume) над задействованной областью памяти: предыдущие записи в зависимую от данных область памяти, сделанные потоком, выполнившим операцию освобождения (release), становятся видимыми для цепочки зависимостей данного потока.
memory_order_acquire Операция загрузки с этим упорядочением памяти выполняет операцию захвата (acquire) над задействованной областью памяти: предыдущие записи, сделанные в зависмую-от-данных область памяти потоком, который выполнил освобождение (release), становятся видимыми в данном потоке.
memory_order_release Операция сохранения с этим упорядочением памяти выполняет операцию освобождения (release): предыдущие записи в другие области памяти, становятся видимыми для потоков, которые выполняют операцию поглощения (consume) или захвата (acquire) над той же областью памяти.
memory_order_acq_rel Операция загрузки с этим упорядочением памяти выполняет операцию захвата (acquire) над задействованной областью памяти. Операция сохранения с этим упорядочением памяти выполняет операцию освобождения (release).
memory_order_seq_cst (sequentially-consistent - последовательно согласованное) То же, что и memory_order_acq_rel, плюс существует единый общий порядок, при котором все потоки видят все изменения (см. ниже) в одинаковом порядке.

[править] Формальное описание

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

[править] Расположено-перед

(Sequenced-before)

В одном и том же потоке, вычисление A расположено-перед вычислением B, если это следует из evaluation order.

[править] Переносит-зависимость-в

(Carries-dependency-to)

В одном и том же потоке, вычисление A, которое расположено-перед вычислением B, может также переносить-зависимость-в B (то есть B зависит от A), если выполняется любое из следующих утверждений:

1) Значение A используется, как операнд B, кроме случаев
a) если B, это вызов std::kill_dependency
b) если A - это левый операнд встроенного оператора &&, ||, ?:, или ,.
2) A пишет в скалярный объект M, B читает из M
3) A переносит-зависимость-в другое вычисление X, и X переносит-зависимость-в B

[править] Порядок изменения

Все изменения любой определённой атомарной переменной происходят в общем порядке, который определён для этой атомарной переменной.

Следующие четыре требования гарантированно выполняются для всех атомарных операций:

1) Запись-запись согласованность: если вычисление A, которое изменяет некоторую атомарную M (запись) происходить-раньше вычисления B, которое изменяем M, тогда A появляется раньше, чем B в порядке изменения M.
2) Чтение-чтение согласованность: если процесс вычисления значения A некоторой атомарной M (чтение) происходит-раньше процесса вычисления значения B этой же M, и если значение A происходит из записи X в M, тогда значение B, это либо значение записанное X, либо значение записанное побочным эффектом Y, воздействующим на M, который возникает позже, чем X, в порядке изменения M.
3) Чтение-запись согласованность: если процесс вычисления значения A некоторой атомарной M (чтение) происходит-раньше операции B применяемой к M (запись), тогда значение A происходит из побочного эффекта (записи) X, который возникает раньше, чем B в порядке модификации M.
4) Запись-чтение согласованность: если побочный эффект (запись) X воздействующий на атомарный объект M происходит-раньше процесса вычисления значения (чтения) B переменной M, тогда вычисление B должно получить своё значение от X или от побочного эффекта Y, который следует за X в порядке модификации M.

[править] Последовательность освобождения

(Release sequence)

После операции освобождения (release) A, выполненной по отношению к атомарному объекту M, самая длинная непрерывная часть последовательности порядка изменения M, которая состоит из:

1) Записей выполненных тем же потоком, который выполнил A
2) Атомарных чтение-изменение-запись операций, применённых любым потоком к M

называется последовательностью освобождения во главе с A.

[править] Предшествует-по-зависимости

(Dependency-ordered before)

Межпоточно вычисление A предшествует-по-зависимости вычислению B, если выполняется любое из следующих утверждений:

1) A выполняет операцию освобождения (release) над некоторой атомарной M, и, в другом потоке, B выполняет операцию поглощения (consume) над той же атомарной M, и B читает значение, записанное любой частью последовательности освобождения во главе с A.
2) A предшестует-по-зависимости X и X переносит-зависимость-в B.

[править] Межпоточно происходит-раньше

(Inter-thread happens-before)

Вычисление A межпоточно происходит-раньше вычисления B, если выполняется любое из следующих утверждений:

1) A синхронизируется-с B
2) A предшествует-по-зависимости B
3) A синхронизируется-с некоторым вычислением X, и X расположено-перед B
4) A расположено-перед некоторым вычислением X, и X межпоточно происходит-раньше B
5) A межпоточно происходит-раньше некоторого вычисления X, и X межпоточно происходит-раньше B

[править] Происходит-раньше

(Happens-before)

В независимости от потоков, вычисление A происходит-раньше вычисления B, если выполняется любое из следующих утверждений:

1) A расположено-перед B
2) A межпоточно происходит-раньше B

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

[править] Видимые побочные эффекты

Побочный эффект A, воздействующий на скалярную M (запись) является видимым по отношению к процессу вычисления значения B переменной M (чтение), если выполняются оба следующих утверждения:

1) A происходит-раньше B
2) Никакой другой побочный эффект X, не действует на M, где A происходит-раньше X, и X происходит-раньше B

Если побочный эффект A является видимым по отношению в процессу вычисления значения B, тогда самое длинное и непрерывное подмножество побочных эффектов воздействующих на M в порядке модификации, где B не происходит-раньше, известено, как “видимая последовательность побочных эффектов” (значение M, определённое B, будет значением, сохранённым одним из этих побочных эффектов).

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

[править] Операция поглощения

(Consume operation)

Атомарная загрузка с упорядочением memory_order_consume или более строгим, является операцией поглощения (consume). Учтите, что барьер std::atomic_thread_fence не является операцией поглощения.

[править] Операция захвата

(Acquire operation)

Атомарная загрузка с упорядочением memory_order_acquire или более строгим, является операцией захвата (acquire). Операция lock(), применяемая к Mutex, также является операцией захвата. Учтите, что барьер std::atomic_thread_fence не является операцией захвата.

[править] Операция освобождения

(Release operation)

Атомарное сохранение (запись) с упорядочением memory_order_release или более строгим, является операцией освобождения (release). Операция unlock(), применяемая к Mutex, также является операцией освобождения. Учтите, что барьер std::atomic_thread_fence не является операцией освобождения.

[править] Объяснение

[править] Ослабленное упорядочение

Атомарные операции, отмеченные как std::memory_order_relaxed, не являются синхронизирующими операциями, они не упорядочивают память. Они гарантируют только атомарность и согласованность порядка модификации.

Например, при x и y изначально равных нулю,

// 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 потому, что хотя A расположено-раньше B и C расположено-раньше D, ничего не мешает D появиться раньше A в порядке изменения y, и B появиться раньше C в порядке изменения x.

(до C++14)

Даже при ослабленной модели памяти, произвольным значениям не разрешено циклически зависеть от вычисления самих себя, например, при x и y изначально равных нулю,

// 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, так как запись 42 в y возможна только, если запись в x записывает 42, и в свою очередь циклически зависит от записи в y, записывающей 42.

(начиная с C++14)

Типичное использование ослабленного упорядочения памяти - это обновление счётчиков, таких как счётчики ссылок в std::shared_ptr, так как оно требуют только атомарности, но не упорядочения или синхронизации.

[править] Упорядочение Освобождение-Захват

Если атомарное сохранение в потоке A отмечено упорядочением std::memory_order_release и атомарная загрузка в потоке B из этой же переменной отмечена упорядочением std::memory_order_acquire, все записи памяти (не атомарные и с ослабленным упорядочением), которые происходят-раньше атомарной записи с точки зрения потока A, становятся видимыми побочными эффектами в потоке B, то есть, после того, как атомарная загрузка завершена, поток B гарантированно увидит всё, что поток A записал в память.

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

На системах со строгим упорядочением (x86, SPARC TSO, IBM) упорядочение освобождение-захват используется автоматически для большинства операций. Для организации данного режима синхронизации не требуется дополнительных инструкций процессора, только некоторые оптимизации компилятора могут оказывать влияние (на порядок инструкций) (например, компилятору запрещено перемещать не атомарные операции записи после (по порядку) атомарных операций записи-освобождения или выполнненять не атомарные операции загрузок до атомарных операций загрузки-захвата). На системах с ослабленным упорядочением (ARM, Itanium, PowerPC), должны использоваться специальные инструкции загрузки (load) процессора или барьеров памяти.

Взаимоисключающие блокировки (такие как std::mutex или atomic spinlock) являются примерами синхронизации вида освобождение-захват: когда блокировка освобождается потоком A и захватывается потоком B, всё, что происходит в критической секции (перед операцией освобождения) в контексте потока A, становится видимым потоку B (после операции освобождения), который выполняет ту же критическую секцию.

#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"); // равенство выполняется всегда
    assert(data == 42); // равенство выполняется всегда
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


Следующий пример демонстрирует транзитивность упорядочения освобождение-захват между тремя потоками

#include <thread>
#include <atomic>
#include <cassert>
#include <vector>
 
std::vector<int> data;
std::atomic<int> flag = {0};
 
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
 
void thread_2()
{
    int expected=1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
        expected = 1;
    }
}
 
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    assert(data.at(0) == 42); // равенство будет выполняться всегда
}
 
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}


[править] Упорядочение Освобождение-Поглощение

Если атомарное сохранение в потоке A отмечено упорядочением std::memory_order_release и атомарная загрузка в потоке B из той же переменной отмечена упорядочением std::memory_order_consume, все записи в память (не атомарные и с ослабленным упорядочением), которые предшествуют-по-зависимостям атомарному сохранению с точки зрения потока A, становятся видимыми побочными эффектами в рамках этих операций в потоке B, в котором операция загрузки переносит-зависимость-в. То есть, когда атомарная операция загрузки завершена, те операторы и функции в потоке B, коротые используют значение, полученное посредством загрузки, гарантированно увидят то, что поток A записал в память.

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

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

См. также std::kill_dependency и [[carries_dependency]] для детального контроля цепочки зависимостей.

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

#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_consume)))
        ;
 
    // равенство выполняется всегда: загрузка ptr переносит-зависимость-в *p2
    assert(*p2 == "Hello");
    // равенство может выполняться, но может и не выполняться, т.к. загрузка из  
    // ptr не переносит-зависимость-в data
    assert(data == 42);
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


[править] Последовательно-согласованных заказа

Если атомная магазин и помечен std::memory_order_seq_cst и атомные нагрузки от той же переменной отмеченных std::memory_order_seq_cst, то операции обладают следующими свойствами:
Оригинал:
If an atomic store and an is tagged std::memory_order_seq_cst and an atomic load from the same variable is tagged std::memory_order_seq_cst, then the operations exhibit the following properties:
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.
  • Не пишет в поток записи могут быть перераспределены после атомной магазина
    Оригинал:
    No writes in the writer thread can be reordered after the atomic store
    Текст был переведён автоматически используя Переводчик Google.
    Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.
  • Нет говорится в потоке, читатель может быть заказана до атомной нагрузки.
    Оригинал:
    No reads in the reader thread can be reordered before the atomic load.
    Текст был переведён автоматически используя Переводчик Google.
    Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.
  • Синхронизация между всеми членами атомарных операций отмеченных std::memory_order_seq_cst. Все темы, используя такие атомарные операции см. тот же порядок доступа к памяти.
    Оригинал:
    The synchronization is established between all atomic operations tagged std::memory_order_seq_cst. All threads using such atomic operation see the same order of memory accesses.
    Текст был переведён автоматически используя Переводчик Google.
    Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.
Последовательный порядок необходимо для многих несколько производитель-потребитель несколько ситуаций, когда все потребители должны наблюдать за действиями всех производителей, входящих в том же порядке.
Оригинал:
Sequential ordering is necessary for many multiple producer-multiple consumer situations where all consumers must observe the actions of all producers occurring in the same order.
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.
Всего последовательное упорядочение требует полной памяти забор инструкций процессора на всех многоядерных систем. Это может стать узким местом, поскольку она заставляет всех доступ к памяти распространяться на каждую нить.
Оригинал:
Total sequential ordering requires a full memory fence CPU instruction on all multi-core systems. This may become a performance bottleneck since it forces all memory accesses to propagate to every thread.
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

[править] Отношения с нестабильной

{{{1}}}
Оригинал:
{{{2}}}
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

[править] Примеры

[править] std::memory_order_relaxed

В следующем примере показано задачи (обновления глобального счетчика), что требует атомарности, но нет заказов ограничений, так как неатомической памяти не участвует .
Оригинал:
The following example demonstrates a task (updating a global counter) that requires atomicity, but no ordering constraints since non-atomic memory is not involved.
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
 
std::atomic<int> cnt = ATOMIC_VAR_INIT(0);
 
void f()
{
    for(int n = 0; n < 1000; ++n) {
        cnt.fetch_add(1, std::memory_order_relaxed);
    }
}
 
int main()
{
    std::vector<std::thread> v;
    for(int n = 0; n < 10; ++n) {
        v.emplace_back(f);
    }
    for(auto& t : v) {
        t.join();
    }
    std::cout << "Final counter value is " << cnt << '\n';
}

Вывод:

Final counter value is 10000

[править] std::memory_order_release and std::memory_order_consume

Этот пример демонстрирует зависимость заказал синхронизации: целочисленных данных не связано с указателем на строку, данные отношения зависимости, при этом его значение не определено в потребительском .
Оригинал:
This example demonstrates dependency-ordered synchronization: the integer data is not related to the pointer to string by a data-dependency relationship, thus its value is undefined in the consumer.
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

#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_consume)))
        ;
    assert(*p2 == "Hello"); // never fires
    assert(data == 42); // may or may not fire
}
 
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); t2.join();
}


[править] std::memory_order_release and memory_order_acquire

Мьютексы, параллельные очереди, и другой производитель-потребитель ситуации требуют упорядочения в релизе издатель нить и приобретают упорядоченность в поток-потребитель. Эта модель устанавливает попарно синхронизации между потоками .
Оригинал:
Mutexes, concurrent queues, and other producer-consumer situations require release ordering in the publisher thread and acquire ordering in the consumer thread. This pattern establishes pairwise synchronization between threads.
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

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


[править] std::memory_order_acq_rel

Последующий пример демонстрирует транзитивной отпустить-продам заказе через три темы
Оригинал:
The follow example demonstrates transitive release-acquire ordering across three threads
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

#include <thread>
#include <atomic>
#include <cassert>
#include <vector>
 
std::vector<int> data;
std::atomic<int> flag = ATOMIC_VAR_INIT(0);
 
void thread_1()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
}
 
void thread_2()
{
    int expected=1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {
        expected = 1;
    }
}
 
void thread_3()
{
    while (flag.load(std::memory_order_acquire) < 2)
        ;
    assert(data.at(0) == 42); // will never fire
}
 
int main()
{
    std::thread a(thread_1);
    std::thread b(thread_2);
    std::thread c(thread_3);
    a.join(); b.join(); c.join();
}


[править] std::memory_order_seq_cst

Этот пример демонстрирует ситуацию, когда последовательное упорядочение необходимо. Любой другой порядок может вызвать утверждать, потому что это было бы возможно для темы c и d наблюдать изменения в Atomics x и y в обратном порядке .
Оригинал:
This example demonstrates a situation where sequential ordering is necessary. Any other ordering may trigger the assert because it would be possible for the threads c and d to observe changes to the atomics x and y in opposite order.
Текст был переведён автоматически используя Переводчик Google.
Вы можете проверить и исправить перевод. Для инструкций щёлкните сюда.

#include <thread>
#include <atomic>
#include <cassert>
 
std::atomic<bool> x = ATOMIC_VAR_INIT(false);
std::atomic<bool> y = ATOMIC_VAR_INIT(false);
std::atomic<int> z = ATOMIC_VAR_INIT(0);
 
void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}
 
void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}
 
void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
        ;
    if (y.load(std::memory_order_seq_cst)) {
        ++z;
    }
}
 
void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
        ;
    if (x.load(std::memory_order_seq_cst)) {
        ++z;
    }
}
 
int main()
{
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    assert(z.load() != 0);  // will never happen
}