2016-03-14 4 views
0

Класс foo имеет метод bar. Согласно некоторому протоколу синхронизации, метод bar конкретного объекта foo будет вызываться только не более чем одним потоком в любой момент времени.Эффективная не-принудительная, проверка, мьютексы

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

class foo { 
public: 
    void bar() { 
     std::lock_guard<verification_mutex> lk{m}; 
     ... 
    } 

private: 
    mutable verification_mutex m; 
}; 

однако, само по себе не обязательно блокировать или разблокировать все. Скорее всего, это будет throw, если обнаружен многопоточный одновременный доступ. Дело в том, чтобы сделать его временной охват как можно более низким (включая его влияние на другой код, например, на memory barriers).

Вот три варианта реализации verification_mutex:

  1. обертка вокруг std::mutex, но с lock реализованного проверкой, что trylock удалось (это просто, чтобы получить представление, очевидно, не очень быстро)
  2. Атомная переменная, обозначающая текущий «блокирующий» поток id, с атомарными операциями exchange (см. Схему реализации ниже).
  3. То же, что и 2, но без атомизма.

Эти правильные или неправильные (в частности, 2 и особенно 3)? Как они повлияют на производительность (например, окружающего кода)? Есть ли вообще превосходная альтернатива?


Редактировать Ответ на @SergeyA ниже это хорошо, но я, в частности, интересно, о барьерах памяти. Решение, не использующее их, было бы замечательным, так как это был бы ответ, дающий некоторое интуитивное объяснение, почему любое решение, исключающее их, обязательно потерпит неудачу.


Эскиз Реализация

#include <atomic>                                                
#include <thread> 
#include <functional> 

class verification_mutex { 
public: 
    verification_mutex() : m_holder{0}{} 

    void lock() { 
     if(m_holder.exchange(get_this_thread_id()) != 0) 
      throw std::logic_error("lock"); 
    } 

    void unlock() { 
     if(m_holder.exchange(0) != get_this_thread_id()) 
      throw std::logic_error("unlock"); 
    } 

    bool try_lock() { 
     lock(); 
     return true; 
    } 

private: 
    static inline std::size_t get_this_thread_id() { 
     return std::hash<std::thread::id>()(std::this_thread::get_id()); 
    } 

private: 
    std::atomic_size_t m_holder; 
}; 
+0

Тройник должен фактически удерживать блокировку, иначе вы не можете определить, есть ли другой «там». Так что это будет не быстрее, чем блокировка. Насколько сильно вам нужно знать об ошибке? Надежно, или это просто тестирование? – Yakk

+0

Почему вы пытаетесь сделать реенрантный замок? – SergeyA

+0

Также обратите внимание, что добавление какой-либо синхронизации приведет к замедлению вашего действия из-за эффекта барьеров памяти (и синхронизация без барьеров памяти бесполезна) – SergeyA

ответ

1

Вариант 3 не является жизнеспособным. При чтении/записи переменной из нескольких потоков вам необходим барьер памяти.

Из всех возможных вариантов атомная булева переменная будет самой быстрой, поскольку она не потребует переключения контекста (возможно, мьютексы). Нечто подобное:

class verifying_mutex { 
    std::atomic<bool> locked{false}; 
public: 
    bool lock() { 
     if (!locked.compare_exchange_strong(false, true)) 
     throw std::runtime_error("Incorrect mt-access pattern"); 
    } 

    bool unlock() { 
     locked = false; 
    } 
}; 

На стороне записки, ваша оригинальная версия замка используется thread_id, которая замедляет работу ненужной. Не делай это.

+0

Хороший вопрос об альтернативе использованию thread_id. Однако это немного компромисс, поскольку идентификаторы потоков могут использоваться для некоторых диагностических программ. –

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