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. Это нарушило бы порядок " происходит до", поэтому не допускается.

В общем, это означает, что компилятор не может поднять что-либо выше загрузки-получения, так как это может нарушить порядок " происходит до".

Другие вопросы по тегам