Атомарный Оператор Присваивания
Я использую очень легкую атомарную оболочку в качестве учебного упражнения для примитивных типов данных в C++ для Windows, и у меня есть несколько простых вопросов о реализации оператора присваивания. Рассмотрим две реализации ниже:
// Simple assignment
Atomic& Atomic::operator=(const Atomic& other)
{
mValue = other.mValue;
return *this;
}
// Interlocked assignment
Atomic& Atomic::operator=(const Atomic& other)
{
_InterlockedExchange(&mValue, other.mValue);
return *this;
}
Предположим, что mValue является правильным типом и чтоатомарный класс имеет его в качестве члена.
- требуется ли
_InterlockedExchangeдля потокобезопасного оператора присваивания, или достаточно простой реализации, чтобы гарантировать потокобезопасность? - Если простое присваивание потокобезопасно, то нужно ли вообще реализовывать оператор присваивания для этого класса? Компилятора по умолчанию должно хватить, не так ли?
- Если простое назначение является потокобезопасным в Windows, является ли оно также потокобезопасным и в других платформах? Требуется ли эквивалент
_InterlockedExchangeдля обеспечения потокобезопасности на других платформах?
3 ответов:
если
mValueэто примитивный тип (и не более 32 бит в ширину на 32-битном процессоре, не более 64 бит в ширину на 64-битном процессоре), и вы работаете на процессоре x86 (в 32-или 64-битном режиме) и Вы не меняете вручную данные, тогда чтение/запись памяти гарантированно будут атомарными.Это само по себе не означает, что компилятор не будет переупорядочивать доступ к памяти или даже полностью оптимизировать его, но процессор гарантирует, что любое хорошо выровненное чтение / запись с помощью данные этих размеров будут атомарными.
Однако обратите внимание, что я говорю об атомарности, а не о потокобезопасности, потому что "потокобезопасность" зависит от контекста, в котором используется код.
Есть Три проблемы, с которыми сталкиваются атомарные типы C++. Во-первых, переключение потока может произойти в середине чтения или записи, что приводит к искажению данных; это известно как "разрыв". Во-вторых, каждый процессор имеет свой собственный кэш данных, и запись значения в одном потоке не обязательно обновляет значение в кэше другого процессора; это "устаревшие данные". В-третьих, компилятор может изменить порядок инструкций для повышения эффективности, если результаты не нарушают различные правила; если вы не говорите об этом то, что данные разделяются между потоками, может удивить вас.
Использование
std::atomic(или различных специфичных для реализации механизмов обеспечения атомарности) решает все три проблемы. Нет никакой веской причиныобходить их; авторы библиотек и компиляторов почти наверняказнают лучше, чем вы, как создавать эффективный код, который работает правильно.
- это зависит от размера и выравнивания
mValue, которое в дальнейшем зависит от того, как объектAtomicвыровнен. Как правило, если размер равен размеру регистра процессора и правильно выровнен, то запись является атомарной. (Пример: 32-разрядный тип данных, выровненный по 32-разрядной границе на 32-разрядном процессоре, будет записан атомарно. Атомарность не гарантируется для 64-разрядного типа данных в этом сценарии.)- Правильно, реализация компилятора по умолчанию будет идентична " простой пример присваивания", который вы привели, предполагая, что
mValueявляется единственным членом данных экземпляра.- обычно да, так как это скорее функция процессора и архитектуры, чем конкретной операционной системы. В тех случаях, когда запись
mValueне будет атомарной, вам потребуется аналогичная конструкция. (Например, смотрите документациюGCC, касающуюся встроенных атомарных операций .)
Comments