Отвечая на этот вопрос, возник еще один вопрос о ситуации с OP, о котором я не знал: в основном это вопрос архитектуры процессора, но с вопросом о том, о модели памяти С++ 11.
В принципе, код OP бесконечно зацикливался на более высоких уровнях оптимизации из-за следующего кода (слегка измененного для простоты):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
где __sync_val_compare_and_swap()
является встроенным GCC атомарным CAS. GCC (разумно) оптимизировал это на бесконечный цикл в случае, когда bits_ & mask
был обнаружен как true
перед входом в цикл, полностью пропустив операцию CAS, поэтому я предложил следующее изменение (которое работает):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
__sync_synchronize();
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
После того, как я ответил, OP отметил, что смена bits_
на volatile uint8_t
также работает. Я предложил не идти по этому маршруту, так как volatile
обычно не следует использовать для синхронизации, и, похоже, в любом случае нет возможности использовать забор здесь.
Однако я думал об этом больше, и в этом случае семантика такова, что на самом деле не имеет значения, если проверка ov & MASK
основана на устаревшем значении, если она не основана на неопределенно устаревшем (т.е. до тех пор, пока цикл прерывается), поскольку фактическая попытка обновления bits_
синхронизируется. Таким образом, существует volatile
достаточно, чтобы гарантировать, что этот цикл завершается, если bits_
обновляется другим потоком, таким как bits_ & MASK == false
, для любого существующего процессора? Другими словами, при отсутствии явного забора памяти практически невозможно, чтобы критические данные, оптимизированные компилятором, не были оптимизированы процессором вместо этого, на неопределенный срок? ( EDIT:. Чтобы быть ясным, я задаю здесь вопрос о том, какое современное оборудование могло бы действительно дать предположение о том, что чтения испускаются в цикле компилятором, поэтому это не технически вопрос языка, хотя его выражают в терминах семантики С++ удобно.)
Это аппаратный угол для него, но немного обновить его и сделать его также ответственным вопросом о модели памяти С++ 11, рассмотрите следующую вариацию кода выше:
// bits_ is "std::atomic<unsigned char>"
unsigned char ov = bits_.load(std::memory_order_relaxed);
while (true) {
if (ov & MASK) {
ov = bits_.load(std::memory_order_relaxed);
continue;
}
// compare_exchange_weak also updates ov if the exchange fails
if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) {
break;
}
}
cppreference утверждает, что std::memory_order_relaxed
подразумевает "никаких ограничений на переупорядочение доступа к памяти вокруг атомной переменной", поэтому независимо от того, что фактическое оборудование будет или не будет делать, подразумевает, что bits_.load(std::memory_order_relaxed)
может технически никогда читать обновленное значение после того, как bits_
обновляется в другом потоке в соответствующей реализации?
EDIT: Я нашел это в стандарте (29.4 p13):
Реализации должны сделать атомные хранилища видимыми для атомных нагрузок в течение разумного промежутка времени.
Таким образом, очевидно, что ожидание "бесконечно долго" для обновленного значения (в основном?) не может быть и речи, но нет никакой надежной гарантии какого-либо определенного временного интервала свежести, кроме этого, должно быть "разумным"; тем не менее, возникает вопрос о фактическом аппаратном поведении.