C++
Модель памяти C ++ 11
Поиск…
замечания
Различные потоки, пытающиеся получить доступ к одному и тому же месту памяти, участвуют в гонке данных, если хотя бы одна из операций является модификацией (также известной как операция хранилища ). Эти расы данных вызывают неопределенное поведение . Чтобы избежать их, необходимо предотвратить одновременное выполнение этими потоками таких конфликтных операций.
Примитивы синхронизации (мьютекс, критический раздел и т. П.) Могут защищать такие обращения. Модель памяти, представленная на C ++ 11, определяет два новых портативных способа синхронизации доступа к памяти в многопоточной среде: атомные операции и заборы.
Атомные операции
Теперь можно читать и записывать в заданную ячейку памяти с помощью операций атомной нагрузки и атомного хранилища . Для удобства они обернуты в класс шаблонов std::atomic<t>
. Этот класс обертывает значение типа t
но на этот раз загружает и сохраняет объект в объект атомарным.
Шаблон не доступен для всех типов. Какие типы доступны, специфичны для реализации, но обычно это большинство (или все) доступных интегральных типов, а также типы указателей. Так что std::atomic<unsigned>
и std::atomic<std::vector<foo> *>
должны быть доступны, а std::atomic<std::pair<bool,char>>
скорее всего, не будет.
Атомные операции имеют следующие свойства:
- Все атомарные операции могут выполняться одновременно из нескольких потоков, не вызывая неопределенного поведения.
- Атомная нагрузка будет видеть либо начальное значение, с которым был сконструирован атомный объект, либо значение, записанное в него через некоторую операцию хранения атома .
- Атомные хранилища к одному и тому же атомному объекту упорядочиваются одинаково во всех потоках. Если поток уже видел значение какой-либо операции с хранилищем атома , последующие операции атомной нагрузки будут видеть либо одно и то же значение, либо значение, сохраненное в результате последующей операции сохранения атома .
- Операции Atomic read-modify-write позволяют получать атомную нагрузку и хранить атом, если нет другого хранилища атомов между ними. Например, можно атомарно увеличивать счетчик от нескольких потоков, и приращение не будет потеряно независимо от конкуренции между потоками.
- Атомные операции получают необязательный параметр
std::memory_order
который определяет, какие дополнительные свойства имеет операция относительно других мест памяти.
станд :: memory_order | Имея в виду |
---|---|
std::memory_order_relaxed | никаких дополнительных ограничений |
std::memory_order_release → std::memory_order_acquire | если load-acquire видит значение, хранящееся в store-release затем сохраняет последовательность до того, как store-release произойдет до того, как нагрузки будут упорядочены после загрузки нагрузки |
std::memory_order_consume | как memory_order_acquire но только для зависимых нагрузок |
std::memory_order_acq_rel | сочетает load-acquire и store-release |
std::memory_order_seq_cst | последовательная согласованность |
Эти теги порядка памяти позволяют использовать три разных дисциплины упорядочения памяти: последовательную согласованность , расслабление и освобождение с помощью его освобождения от сестры.
Последовательная последовательность
Если для атомной операции не указан порядок памяти, порядок по умолчанию соответствует последовательной согласованности . Этот режим также может быть явно выбран пометкой операции с помощью std::memory_order_seq_cst
.
При таком порядке операция памяти не может пересекать атомную операцию. Все операции с памятью, секвентированные до атомной операции, происходят до атомной операции, а атомная операция выполняется перед всеми операциями памяти, которые после нее выполняются. Этот режим, пожалуй, самый простой способ рассуждать, но это также приводит к наибольшему штрафу за производительность. Он также предотвращает все оптимизаторы компилятора, которые в противном случае могли бы попытаться изменить порядок операций над атомной операцией.
Расслабленный заказ
Противоположное последовательной непротиворечивость расслабленного упорядочение памяти. Он выбирается с тегом std::memory_order_relaxed
. Устраненная атомная операция не накладывает никаких ограничений на другие операции с памятью. Единственный эффект, который остается, заключается в том, что операция сама по себе является атомарной.
Заказ на выпуск-Приобретение
Операцию атомного хранилища можно std::memory_order_release
с помощью std::memory_order_release
а операцию атомной нагрузки можно std::memory_order_acquire
с помощью std::memory_order_acquire
. Первая операция называется (атомная) store-release, а вторая называется (атомарной) загрузкой-загрузкой .
Когда load-purchase видит значение, записанное в магазине-релизе, происходит следующее: все операции хранения, упорядоченные до того, как хранилище становится видимым для операций загрузки (которые произошли до ), которые упорядочиваются после загрузки нагрузки .
Операции Atomic read-modify-write также могут принимать кумулятивный тег std::memory_order_acq_rel
. Это делает атомную порции нагрузки на операциях атомных нагрузки-то время приобретает атомный магазин участок становится атомным магазин-релиз.
Компилятору не разрешается перемещать операции хранения после операции хранения в хранилище атомов . Также не допускается перемещать операции загрузки до получения атомной нагрузки (или нагрузки ).
Также обратите внимание на то, что не существует атомной загрузки нагрузки или приобретения атомарного хранилища . Попытка создания таких операций делает их расслабленными .
Заказ релиза-потребления
Эта комбинация похожа на release-приобретать , но на этот раз атомная нагрузка помечена std::memory_order_consume
и становится (атомной) нагрузкой . Этот режим является таким же, как и при получении освобождения, с той лишь разницей, что между операциями загрузки, упорядоченными после потребления нагрузки, только они зависят от значения, загруженного потреблением нагрузки .
Заборы
Заборы также позволяют упорядочивать операции памяти между потоками. Забор - это либо забор, либо забор.
Если забор забор происходит до захвата забора, тогда сохраняются секвенсированные до того, как заборный забор будет видимым для нагрузок, секционированных после захвата забора. Чтобы гарантировать, что забор для выпуска происходит до получения забора, можно использовать другие примитивы синхронизации, включая расслабленные атомные операции.
Необходимость в модели памяти
int x, y;
bool ready = false;
void init()
{
x = 2;
y = 3;
ready = true;
}
void use()
{
if (ready)
std::cout << x + y;
}
Один поток вызывает функцию init()
а другой поток (или обработчик сигнала) вызывает функцию use()
. Можно было бы ожидать, что функция use()
будет либо печатать 5
либо ничего не делать. Это может быть не всегда по нескольким причинам:
ЦП может переупорядочить записи, которые происходят в
init()
так что исполняемый код может выглядеть так:void init() { ready = true; x = 2; y = 3; }
ЦП может изменить порядок чтения, которое происходит при
use()
так что фактически исполняемый код может стать:void use() { int local_x = x; int local_y = y; if (ready) std::cout << local_x + local_y; }
Оптимизирующий компилятор C ++ может решить переупорядочить программу аналогичным образом.
Такое переупорядочение не может изменить поведение программы, выполняемой в одном потоке, потому что поток не может чередовать вызовы init()
и use()
. С другой стороны, в многопоточной настройке один поток может видеть часть записей, выполняемых другим потоком, где может случиться, что use()
может видеть ready==true
и мусор в x
или y
или y
и другое.
Модель памяти C ++ позволяет программисту указать, какие операции переупорядочения разрешены, а какие нет, чтобы многопоточная программа также могла вести себя так, как ожидалось. Приведенный выше пример можно переписать в потокобезопасном виде следующим образом:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
ready.store(true, std::memory_order_release);
}
void use()
{
if (ready.load(std::memory_order_acquire))
std::cout << x + y;
}
Здесь init()
выполняет операцию хранения с использованием атома . Это не только сохраняет значение true
в ready
, но также сообщает компилятору, что он не может переместить эту операцию перед выполнением операций записи, которые были секвенированы до нее.
Функция use()
выполняет операцию получения атомарной нагрузки . Он считывает текущее значение ready
а также запрещает компилятору размещать операции чтения, которые секвенированы после того, как это произойдет до получения атомной нагрузки .
Эти атомические операции также заставляют компилятор помещать любые инструкции аппаратного обеспечения, чтобы сообщить ЦП воздерживаться от нежелательных переупорядочений.
Поскольку атомный хранилище-хранилище находится в том же месте памяти, что и при получении атомной нагрузки , модель памяти предусматривает, что если операция загрузки нагрузки видит значение, записанное в операции хранения-хранилища , то все записи, выполняемые init()
' s до этого хранилища будет видна для загрузок, которые use()
поток use()
после его загрузки . То есть, если use()
видит ready==true
, тогда гарантировано увидеть x==2
и y==3
.
Обратите внимание, что компилятору и процессору по-прежнему разрешено записывать в y
до записи на x
, и аналогично чтение этих переменных в use()
может происходить в любом порядке.
Пример забора
Приведенный выше пример также может быть реализован с помощью заборов и расслабленных атомных операций:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
atomic_thread_fence(std::memory_order_release);
ready.store(true, std::memory_order_relaxed);
}
void use()
{
if (ready.load(std::memory_order_relaxed))
{
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
}
Если операция атомной нагрузки видит значение, записанное в хранилище атомов, то происходит до загрузки, а также ограждения: забор заготовки происходит до того, как забор заготовки делает запись в x
и y
которые предшествуют заборному ограждению, чтобы стать видимым к выражению std::cout
которое следует за заборщиком.
Забор может быть полезен, если он может уменьшить общее количество операций получения, выпуска или других операций синхронизации. Например:
void block_and_use()
{
while (!ready.load(std::memory_order_relaxed))
;
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
Функция block_and_use()
вращается, пока флаг ready
будет установлен с помощью ослабленной атомной нагрузки. Затем для обеспечения необходимого порядка памяти используется один забор.