Для начала рассмотрим семантику релиза. Если набор данных защищен спин-блокировкой (мьютекс и т. Д.), Не имеет значения, какая именно реализация используется; пока предположим, что 0 означает, что он свободен, а 1 - занят). После изменения набора данных поток сохраняет 0 для адреса спин-блокировки. Чтобы обеспечить видимость всех предыдущих действий перед сохранением 0 для адреса спин-блокировки, сохранение выполняется с семантикой освобождения, что означает, что все предыдущие чтения и записи должны быть видны другим потокам перед этим сохранением. Это детали реализации, независимо от того, делается ли это с полным барьером или с отметкой об освобождении от одной операции хранения. Это (я надеюсь) понятно без всяких сомнений.
Затем рассмотрим их момент, когда захватывается владение спин-блокировкой. Для защиты от гонки это любая операция сравнения и настройки. При реализации CAS с одной инструкцией (X86, Sparc ...) это объединяет чтение и запись. То же самое для X86 Atomic XCHG. С LL / SC (большинство RISC) это падает до:
- Считывание (LL) местоположения спин-блокировки, пока оно не покажет свободное состояние. (Может быть оптимизировано с некоторой задержкой процессора.)
- Запись (SC) значения «занято» (в нашем случае 1). CPU показывает, была ли операция успешной (флаг условия, регистр вывода и т. Д.)
- Проверьте результат записи (SC) и, если не удалось, перейдите к шагу 1.
Inво всех случаях операция, которая должна быть видимой другим потокам, чтобы показать, что спин-блокировка занята, записывает 1 в ее местоположение, и между этой записью и последующими манипуляциями с набором данных, защищенных спин-блокировкой, должен быть установлен барьер. Чтение этого спин-блокировки ничего не дает схеме защиты, кроме разрешения работы CAS или LL / SC.
Но все реально реализованные схемы позволяют получить модификацию семантики для операций чтения (или CAS), а не записи. В результате для схемы LL / SC потребуется дополнительная заключительная операция чтения с получением на спин-блокировке для фиксации необходимого барьера. Но в типичном выводе такой инструкции нет. Например, если скомпилировать на ARM:
for(;;) {
int e{0};
int d{1};
if (std::atomic_compare_exchange_weak_explicit(p, &e, d,
std::memory_order_acquire,
std::memory_order_relaxed)) {
return;
}
}
, его выходные данные сначала содержат LDAXR == LL +, а затем STXR == SC (без барьера, поэтому нет гарантии, что другие потоки увидят его?) Вероятно, это не мой артефакт, но он генерируется, например, в glibc: pthread_spin_trylock
вызывает __atomic_compare_exchange_weak_acquire
(и не более барьеров), который попадает во встроенную GCC __atomic_compare_exchange_n
с захватом при чтении мьютекса и без освобождения при записи мьютекса.
Кажется, я упустил некоторые основные детали в этом рассмотрении. Кто-нибудь исправит это?
Это также может попасть в 2 подвопроса:
SQ1: В последовательности инструкций, например:
(1) load_linked+acquire mutex_address ; found it is free
(2) store_conditional mutex_address ; succeeded
(3) read or write of mutex-protected area
, что предотвращает переупорядочение ЦП (2) и (3), в результате чего другие потоки не увидят, что мьютекс заблокирован?
SQ2: существует ли фактор проектирования, который предполагает использование семантики только для нагрузок?
Видно, что некоторые примеры кода без блокировки, такие как:
поток 1:
var = value;
flag.store(true, std::memory_order_release);
поток 2:
if (flag.load(std::memory_order_acquire)) {
// We already can access it!!!
value = var;
... do something with value ...
}
, но это должно было быть сделано работать после стиль, защищенный мьютексом, работает стабильно.