C++11: Что мешает магазинам подниматься выше начала критической секции блокировки?
Насколько я понимаю, спин-блокировка может быть реализована с использованием атомарности C++11 с помощью acqu-CAS при блокировке и release-store при разблокировке, что-то вроде этого:
class SpinLock {
public:
void Lock() {
while (l_.test_and_set(std::memory_order_acquire));
}
void Unlock() {
l_.clear(std::memory_order_release);
}
private:
std::atomic_flag l_ = ATOMIC_FLAG_INIT;
};
Рассмотрим его использование в функции, которая получает блокировку, а затем выполняет слепую запись в какое-то общее местоположение:
int g_some_int_;
void BlindWrite(int val) {
static SpinLock lock_;
lock_.Lock();
g_some_int_ = val;
lock_.Unlock();
}
Меня интересует, как компилятор ограничен в переводе этого в сгенерированный ассемблерный код.
Я понимаю, почему пишу g_some_int_
не может мигрировать после конца критического раздела в выходных данных компилятора - это будет означать, что запись не гарантируется для следующего потока, который получает блокировку, что гарантируется порядком освобождения / получения.
Но что мешает компилятору переместить его до приобретения CAS флага блокировки? Я думал, что компиляторам было разрешено переупорядочивать записи в разные области памяти при генерации кода сборки. Есть ли какое-то специальное правило, которое предотвращает переупорядочение записи перед атомарным хранилищем, которое предшествует ей в программном порядке?
Я ищу ответ адвоката по языку здесь, желательно охватывающий std::atomic
так же как std::atomic_flag
,
Изменить, чтобы включить что-то из комментариев, что, возможно, задает вопрос более четко. Суть вопроса в том, какая часть стандарта говорит, что абстрактная машина должна соблюдать l_
являющийся false
прежде чем он пишетg_some_int_
?
Я подозреваю, что ответ либо "записи не могут быть сняты выше потенциально бесконечных циклов", либо "записи не могут быть сняты выше атомарных записей". Возможно, это даже "ты ошибаешься, что записи вообще можно переупорядочить". Но я ищу конкретную ссылку в стандарте.
1 ответ
Предположим, у вас есть две функции, которые используют ваш спинлок:
SpinLock sl;
int global_int=0;
int read(){
sl.Lock();
int res=global_int;
sl.Unlock();
return res;
}
void write(int val){
sl.Lock();
global_int=val;
sl.Unlock();
}
Если два звонка BlindWrite
происходить одновременно в отдельных потоках, тогда один (назовите его A) получит блокировку; другой (B) будет вращаться в петле в Lock
,
А потом пишет g_some_int
и звонки Unlock
, который содержит вызов clear
который является магазин-релиз. Запись упорядочена до вызова clear
так как это в той же теме.
B затем просыпается в Lock
и на этот раз test_and_set
возврат звонка false
, Это захват загрузки, который считывает значение, сохраненное при вызове clear
так что призыв к clear
синхронизируется с этим вызовом test_and_set
,
test_and_set
вызывать Lock
является загрузкой, и она упорядочена перед записью в g_some_int
в BlindWrite
, так как это в той же теме.
Так как первая запись в потоке A упорядочена до вызова clear
, который синхронизируется с вызовом test_and_set
в потоке B, который, в свою очередь, чередуется - перед записью в потоке B, запись в потоке A происходит - перед записью в потоке B.
Если компилятор поднял запись в g_some_int
над призывом к Lock
, тогда было бы возможно, чтобы запись из потока B происходила до записи в поток A. Это нарушило бы порядок " происходит до", поэтому не допускается.
В общем, это означает, что компилятор не может поднять что-либо выше загрузки-получения, так как это может нарушить порядок " происходит до".