2015-09-18 3 views
2

Скажет, например, у меня есть реализация блокировки эксклюзивный атомарный-OPS на основе спин, как показано ниже:Можно ли установить блокировку блокировки блокировки на основе атома, чтобы установить флаг блокировки в ноль?

bool TryLock(volatile TInt32 * pFlag) 
{ 
    return !(AtomicOps::Exchange32(pFlag, 1) == 1); 
} 

void Lock (volatile TInt32 * pFlag) 
{ 
    while (AtomicOps::Exchange32(pFlag, 1) == 1) { 
     AtomicOps::ThreadYield(); 
    } 
} 

void Unlock (volatile TInt32 * pFlag) 
{ 
    *pFlag = 0; // is this ok? or here as well a atomicity is needed for load and store  
} 

Где AtomicOps::Exchange32 осуществляется на окнах с помощью InterlockedExchange и на Linux с помощью __atomic_exchange_n.

+0

Связанные вопросы: http://stackoverflow.com/questions/1383363/is-my-spin-lock-implementation-correct-and-optimal http://stackoverflow.com/questions/6810733/do-spin-locks-always -require-a-memory-барьер-это-spinning-on-a-memory-барьер-e http://stackoverflow.com/questions/26307071/does-the-c-volatile-keyword-introduce-a-memory- забор – gavv

+0

Можете ли вы пожалуйста, дополните? Почему именно здесь мне нужен барьер памяти? Что пошло не так, и как, если я его не использую? –

+0

'' 'Lock()' '' требует «получить барьер», чтобы гарантировать, что все изменения, сделанные при блокировке блокировки, будут применяться только после обновления '' 'pFlag'''. '' 'Unlock()' '' требует «барьера выпуска», чтобы гарантировать, что все изменения, сделанные при блокировке спин-блокировки, будут применены до обновления '' 'pFlag'''. См. Подробности здесь: https://jfdube.wordpress.com/2012/03/08/understanding-memory-ordering/.Это общий подход, но на x86 вам понадобятся только барьеры компилятора вместо барьеров для приобретения и выпуска; см. здесь: http://preshing.com/20120913/acquire-and-release-semantics/ – gavv

ответ

1

Вам нужно два барьера памяти в реализации:

спин-блокировки
  • «барьер» сбора по или «импортный барьер» в TryLock() и Lock(). Он выдает операции, выданные, в то время как спин-блокировка становится видимой только после того, как значение pFlag обновлено.
  • "барьер выпуска" или "экспортный барьер" в Unlock(). Он заставляет операции выдавать до тех пор, пока spinlock не будет выпущен, чтобы быть видимым до того, как значение pFlag будет обновлено.

Вам также нужны два барьера для компилятора по тем же причинам.

Подробнее см. this article.


Этот подход предназначен для общего случая. По состоянию на x86/64:

  • нет отдельных ограждений для получения/выпуска, но только один полный барьер (ограждение памяти);
  • нет необходимости в барьерах памяти здесь вообще, поскольку эта архитектура строго упорядочена;
  • вам все еще нужен компилятор барьеры.

Дополнительные сведения предоставлены here.

Ниже приведен пример реализации с использованием GCC atomic builtins. Он будет работать для всех архитектур, поддерживаемых GCC:

  • это вставит барьеры памяти приобретает/релиз на архитектуры, где они необходимы (или полный барьере, если приобретать/отпускание барьеры не поддерживаются, но архитектура слабо заказана);
  • он будет вставлять барьеры компилятора во все архитектуры.

Код:

bool TryLock(volatile bool* pFlag) 
{ 
    // acquire memory barrier and compiler barrier 
    return !__atomic_test_and_set(pFlag, __ATOMIC_ACQUIRE); 
} 

void Lock(volatile bool* pFlag) 
{ 
    for (;;) { 
     // acquire memory barrier and compiler barrier 
     if (!__atomic_test_and_set(pFlag, __ATOMIC_ACQUIRE)) { 
      return; 
     } 

     // relaxed waiting, usually no memory barriers (optional) 
     while (__atomic_load_n(pFlag, __ATOMIC_RELAXED)) { 
      CPU_RELAX(); 
     } 
    } 
} 

void Unlock(volatile bool* pFlag) 
{ 
    // release memory barrier and compiler barrier 
    __atomic_clear(pFlag, __ATOMIC_RELEASE); 
} 

Для "расслабленном ожидания" петли, см this и this вопросы.

См. Также Linux kernel memory barriers в качестве хорошей справки.


В вашей реализации:

  • Lock() называет AtomicOps::Exchange32(), который уже включает в себя компилятор барьер и, возможно, приобрести или полный барьер памяти (мы не знаем, потому что вы не предоставили фактические аргументы __atomic_exchange_n()).
  • Unlock() пропускает как память, так и барьеры компилятора, так что он сломан.

Также рассмотрите возможность использования pthread_spin_lock(), если это вариант.

+0

Под «барьерами оптимизации» вы подразумеваете барьеры компилятора? В ответе уже упоминается, что барьеры компилятора всегда необходимы, но я добавлю пояснения. – gavv

+0

Как гарантируется, что цикл «эффективного ожидания» в конечном итоге выйдет без барьера для приобретения? – 5gon12eder

+0

@ 5gon12eder, не '' 'volatile''' достаточно здесь? Существуют ли архитектуры, где запись, сделанная одним процессором, не видна другому ЦП без барьера памяти? (так что они не обеспечивают когерентность кэширования?) – gavv

1

В большинстве случаев для освобождения ресурса просто сброс блокировки на нуль (как и у вас) почти полностью (например, на процессоре Intel Core), но вам также нужно убедиться, что компилятор не будет обмениваться инструкциями (см. ниже, см. также сообщение gv). Если вы хотите быть строгим (и портативные), есть две вещи, которые необходимо учитывать:

Что компилятор делает: Он может обмениваться инструкции по оптимизации кода, и, таким образом, ввести некоторые тонкие ошибки, если это не «осознавая» многопоточный характер кода. Чтобы этого избежать, можно вставить барьер компилятора.

Что делает процессор: Некоторые процессоры (такие как Intel Itanium, используемые на профессиональных серверах или процессоры ARM, используемые в смартфонах) имеют так называемую «модель ослабленной памяти». На практике это означает, что процессор может принять решение об изменении порядка операций. Опять же, этого можно избежать, используя специальные инструкции (барьер нагрузки и барьер хранения). Например, в процессоре ARM, то DMB инструкция гарантирует, что все операции магазина завершены до следующей инструкции (и она должна быть вставлена ​​в функции, которая освобождает замок)

Вывод: Это очень сложно сделайте код правильным, если у вас есть поддержка некоторых компиляторов/ОС для этих функций (например, stdatomics.h или std::atomic в C++ 0x), гораздо лучше полагаться на них, чем написание собственных (но иногда у вас нет выбора) , В конкретном случае стандартного процессора Intel Core я считаю, что то, что вы делаете, является правильным, если вы вставляете компилятор-барьер в операцию выпуска (см. Сообщение g-v).

На время компиляции по сравнению с упорядочением памяти времени выполнения, см: https://en.wikipedia.org/wiki/Memory_ordering

Мой код для некоторых атомных/спин-блокировок, реализованных на различных архитектурах: http://alice.loria.fr/software/geogram/doc/html/atomics_8h.html (но я не уверен, что это 100% правильно)

+2

«В конкретном случае стандартного процессора Intel Core я считаю, что вы делаете правильно». - Как упоминалось в комментариях и моем ответе, это * не * без барьера компилятора ('' volatile'' здесь недостаточно). С барьером компилятора он будет правильным для x86. – gavv

+0

Да, вы правы, я редактировал сообщение. – BrunoLevy

Смежные вопросы