Взаимоблокировка в реализации блокировки MCS

#multithreading #locking #x86-64 #atomic #inline-assembly

#многопоточность #блокировка #x86-64 #атомарный #встроенная сборка

Вопрос:

Аппаратное обеспечение:
Версия ядра Darwin 13.2.0: чт, 17 апреля, 23:03:13 PDT 2014; root: xnu-2422.100.13 ~ 1/RELEASE_X86_64 x86_64
 atomics.hpp

 1 #ifndef ATOMIC_UTILS_H
 2 #определить ATOMIC_UTILS_H
 3
 4 #включить
 5 
 6 #определить БАРЬЕР() __asm__ volatile ( "": : :"память")
 7
 8 #определить CPU_RELAX() __asm__ volatile( "пауза  n  t": : :"память")
 9 
 10 #определить STORE_FENCE() __asm__ volatile("защита" ::: "память");
 11 
 12 классов AtomicUtils
 13 {
 14 общедоступных:
 15
 16 /**
 17 * проверьте, равно ли значение в addr старому значению, если да, замените его на newva l 
 18 * и возвращает старое значение
 19 */
 20 встроенных статических параметров size_t compareAndExchange (volatile size_t* addr, size_t oldval, size_t newval)
 21 {
 22 size_t ret;
 23 __asm__ volatile( "заблокировать cmpxchgq %2, %1 n t"
 24: "=a"(ret), " m"(*addr)
 25: "r" (новое значение), "0" (старое значение)
 26 : "память"); 
 27 возвращает ret;
 28 }
 29
 30 /**
 31 * Атомарно сохраняет x в addr и возвращает предыдущее
 32 * сохранено в addr
 33 */
 34 встроенный статический размер_t loadAndStore ( размер_t x, изменяемый размер_t* addr)
 36 {
 37 size_t ret;
 38 __asm__ volatile( "заблокировать xchgq %1, %0 n t"
 39: " m"(*addr), "=r"(ret)
 40: "1"(x) );
 41 возврат ret;
 42 }
 43
 44 };
 45 
 46 #endif
 mcs.hpp

 1 #ifndef MCS_LOCK_H
 2 #определить MCS_LOCK_H
 3
 4 #включить "atomics.hpp"
 5 #включить 
 6 
 7 класс MCSLock
 8 {
 9 структура mcs_lock_t 
 10 {
 11 mcs_lock_t(): следующий (0), заблокирован (false){}
 12 struct mcs_lock_t* далее;
 13 bool заблокирован;
 14 }; 
15 
 16 общедоступных:
 17 структура typedef mcs_lock_t mcs_lock;
 18 
 19 частных:
 20 mcs_lock** хвост;
 21 статическое усиление::thread_specific_ptr tls_node;
 22 
 23 общедоступных:
 24 MCSLock ( mcs_lock** lock_tail ): хвост ( lock_tail)
 25 {
 26 if( tls_node.get() == 0 )
 27 tls_node.reset( новый mcs_lock());
 28 }
 29 
 30 аннулирует блокировку()
 31 {
 32 mcs_lock* thread_node = tls_node.get();
 33_нод потока-> следующий = 0;
 34_нод потока->заблокирован = true;
 35 
 36 volatile mcs_lock* pred = reinterpret_cast(
 37 AtomicUtils::loadAndStore(
 38_интерпретация (thread_node), 
 39 reinterpret_cast(хвост)
 40 )
 41 );
 42 if( pred != 0)
 43 {
 44 pred-> next = *хвост;
 45 
 46 STORE_FENCE();
 47 //БАРЬЕР(); // Требуется для предотвращения изменения порядка между prev-> next = tail и thread_node->заблокирован. ( Р. Р. Харзард)
 48 
 49 // Запуск локальной переменной. Кто-нибудь, разблокируйте меня, пожалуйста!!
 50 в то время как ( thread_node->заблокирован)
 51 CPU_RELAX(); 
52 
 53 }
 54 }
 55 
 56 аннулирование разблокировки ()
 57 {
 58 mcs_lock* thread_node = tls_node.get(); 
 59 если( thread_node->next == 0)
 60 {
 61 // Если false, то у нового потока есть запрос на блокировку. Теперь снимите блокировку для нового потока 
 62 если(
 63 AtomicUtils::compareAndExchange(
 64 reinterpret_cast(хвост), 
 65_интерпретация (thread_node), 
 66 0 
 67 ) == reinterpret_cast( thread_node ) 68)
 69 {
 70 возврат;
 71 }
 72 
 73 в то время как( thread_node->next == 0)
 74 CPU_RELAX();
 75 }
 76 
 77_нод потока-> следующий-> заблокирован = false;
 78 }
 79}; 
80 
 81 boost::thread_specific_ptr MCSLock::tls_node;
 82 #endif
 mcs_test.cpp

  1 #include "mcs.hpp"
  2 #include <iostream>
  3 #include <pthread.h>
  4 #include <vector>
  5 #define NUM_THREADS 16
  6 #define NUM_ITERATIONS 100
  7
  8 std::vector<int> elements;
  9 MCSLock::mcs_lock *tail = 0;
 10
 11 void* thread_run( void* data )
 12 {
 13   MCSLock lock( amp;tail );
 14   for( int i = 0; i < NUM_ITERATIONS;   i )
 15   {
 16       lock.lock();
 17       elements.push_back( i );
 18       lock.unlock();
 19   }
 20
 21   return 0;
 22 }
 23
 24 int main()
 25 {
 26     pthread_t threads[ NUM_THREADS ];
 27     elements.reserve( NUM_THREADS * NUM_ITERATIONS );
 28
 29     {
 30         for( int i = 0; i < NUM_THREADS;   i )
 31             pthread_create( amp;threads[i], NULL, thread_run, NULL );
 32
 33         for( int i = 0; i < NUM_THREADS;   i )
 34             pthread_join( threads[i], NULL );
 35
 36         std::cout <<"nExiting main thread: " << std::endl;
 37     }
 38 }
  

Приведенный выше код скомпилирован с использованием clang

Проблема:

Я вижу, что 1 или 2 потока застряли в lock () в строке 50. Кроме основных потоков, потоков, которые застряли в lock(), других живых потоков нет. Это означает, что когда другие потоки вызывают unlock(), они каким-то образом не устанавливают значение locked = false для других переменных и не завершают работу.

Какие-либо указания по отладке этого, пожалуйста?

Застрял на этом много часов и никаких подсказок.

Комментарии:

1. К вашему сведению, перенос ваших собственных блокировок с помощью встроенного asm обычно бессмысленен. Я мог бы увидеть преимущество написания вашей собственной очереди без блокировки, но использование блокировок библиотеки вокруг обычной очереди имело бы больше смысла. Даже для очереди без блокировки используйте C 11 std::atomic вместо ручного развертывания собственных примитивов поверх изменяемых объектов.

2. @PeterCordes: Я думаю, стоит рассмотреть это в контексте. Об этом спрашивали 5-6 лет назад. Большинство стабильных версий дистрибутивов не использовали GCC, который поддерживал атомарные примитивы C 11. Когда-то вам приходилось делать собственный откат.

3. @MichaelPetch: Я видел дату в вопросе, через 3 года после C 11. Да, справедливо отметить, что std:: atomic был относительно новым на тот момент, но в то время это был определенно правильный выбор, даже если это означало установку обновленного набора инструментов. Что еще более важно, это определенно правильный выбор сейчас для любых нынешних / будущих читателей вопроса.

Ответ №1:

Разве в clang нет встроенных модулей для этих встроенных блоков asm (например, __sync_val_compare_и_swap в gcc)? Зачем заново изобретать колесо?

Во-вторых, я бы действительно подумал о добавлении переполнения памяти в loadAndStore. Вам нужно убедиться, что все записи, которые компилятор хранит в регистрах, сбрасываются в память перед выполнением xchgq. Аналогично, это не позволит gcc оптимизировать операции чтения в памяти до xchgq. И то, и другое было бы плохо.

В-третьих, я бы проверил вывод asm для ваших циклов while (thread_node->заблокирован и thread_node-> следующий). Поскольку эти переменные не являются изменчивыми, gcc может оптимизировать это, чтобы выполнить чтение только один раз.

Возможно, это не решит вашу проблему, но именно с этого я бы начал.