Я читаю о DCLP (дважды проверенная схема блокировки), и я не уверен, что понял правильно. При использовании атомики для создания блокировки (как описано в DCLP исправлено в C ++ 11), и есть 2 вещи, которые не ясны:
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_acquire); if (tmp == nullptr) { std::lock_guard<std::mutex> lock(m_mutex); tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; m_instance.store(tmp, std::memory_order_release); } } return tmp; }
Что произойдет, если я найду забор внутри «load ()», но чем tmp не будет nullptr, и я просто вернусь? Разве мы не должны указывать, где процессор может «снять ограждение»?
И если нет необходимости снимать ограждение, то почему мы должны приобретать и освобождать? в чем разница?
Ужасно мне не хватает чего-то простого ….
Singleton* Singleton::m_instance = null; std::atomic<bool> Singleton::is_first; // init to false std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { bool tmp = is_first.load(std::memory_order_acquire); if (tmp == false) { std::lock_guard<std::mutex> lock(m_mutex); tmp = is_first.load(std::memory_order_relaxed); if (tmp == false) { // can place any code that will run exactly once! m_instance = new Singleton; // store back the tmp atomically is_first.store(tmp, std::memory_order_release); } } return m_instance; }
Другими словами, вместо того, чтобы смотреть на экземпляр, я использую атомарное логическое значение, чтобы убедиться, что DCLP работает, и все, что находится внутри второго tmp, должно быть синхронизировано и выполнено один раз. Это правильно?
Спасибо!
РЕДАКТИРОВАТЬ: Обратите внимание, я не задаю вопрос для реализации синглтон, но просто, чтобы лучше понять концепции заборов и атомных и как это исправлено DCLP. Это теоретический вопрос.
Что произойдет, если я найду забор внутри «load ()», но чем tmp не будет nullptr, и я просто вернусь? Разве мы не должны указывать, где процессор может «снять ограждение»?
Нет, релиз делается, когда магазин в m_instance
случается. Если вы загружаете m_instance
и это не ноль, тогда релиз уже произошел ранее, и вам не нужно это делать.
Вы не «приобретаете забор» и «освобождаете забор», как если бы вы приобрели мьютексный замок. Это не то, что заборы. Забор — это просто операция получения или освобождения без соответствующей ячейки памяти. И заборы здесь не очень актуальны, потому что все операции получения и выпуска имеют связанную область памяти (атомарный объект m_instance
).
Вам не нужно приобретать + отпускать в согласованных парах, таких как блокировки мьютекса + разблокировки. Вы можете сделать одну операцию освобождения, чтобы сохранить значение и иметь любое количество операций получения (ноль или более), которые загружают это значение и наблюдают за его эффектами.
Семантика получения / выпуска в загрузках / хранилищах связана с упорядочением операций на любой стороне загрузки / хранилища, чтобы предотвратить повторный заказ.
Непринужденное атомарное хранилище (то есть операция освобождения) для переменной A будет синхронизировать с более поздняя неослабленная атомная нагрузка (то есть операция получения) той же переменной A.
Как говорит стандарт C ++:
Неофициально, выполняя операцию разъединения на стороне А, предшествующей
влияние на другие области памяти, чтобы стать видимым для других потоков, которые позже выполняют операцию получения или получения на A.
Итак, в приведенном вами коде DCLP m_instance.store(tmp, memory_order_release)
это магазин для m_instance
и является операцией освобождения. m_instance.load(memory_order_acquire)
нагрузка от m_instance
и является операцией приобретения. Модель памяти говорит, что хранилище ненулевого указателя синхронизируется с любой загрузкой, которая видит ненулевой указатель, что означает, что гарантируется, что все эффекты new Singleton
завершено, прежде чем любой поток может загрузить ненулевое значение из tmp
, Это устраняет проблемы, связанные с двойной проверкой блокировки до C ++ 11, когда хранилище tmp
может стать видимым для других потоков, прежде чем объект будет полностью построен.
Другими словами, вместо того, чтобы смотреть на экземпляр, я использую атомарное логическое значение, чтобы убедиться, что DCLP работает, и все, что находится внутри второго tmp, должно быть синхронизировано и выполнено один раз. Это правильно?
Нет, потому что вы храните false
Вот:
// store back the tmp atomically
is_first.store(tmp, std::memory_order_release);
Что означает при следующем вызове функции вы создаете другую Singleton
и утечка первого. Так должно быть:
is_first.store(true, std::memory_order_release);
Если вы исправите это, я думаю, что это правильно, но в типичных реализациях он использует больше памяти (sizeof(atomic<bool>)+sizeof(Singleton*)
вероятно, больше, чем sizeof(atomic<Singleton*>)
), и, разбив логику на две переменные (логическое значение и указатель), вы упростите ошибку, как и раньше. Таким образом, нет никакого преимущества в этом способе по сравнению с оригиналом, где сам указатель также служит логическим значением, потому что вы смотрите непосредственно на указатель, а не на какое-то логическое значение, которое могло быть установлено неправильно.