Правильный C++ способ реализации lock_guard для пользовательской библиотеки
О, мудрые сети
Между двумя коллегами мы зашли в тупик, и мы могли бы использовать вашу помощь для правильного решения C++. По сути, у нас есть набор служебных классов, два из которых - классы Mutex и SpinLock, оба имеют следующий сокращенный интерфейс:
class Mutex {
public:
Mutex();
~Mutex();
void Lock();
void Unlock();
// ...
};
Очевидно, что это похоже на концепцию BasicLockable, используемую std::lock_guard, но отличается от нее, поэтому мы хотим что-то похожее (предположим, что класс Mutex в этом примере неизменен; мы не можем добавить концепцию BasicLockable к нему). Также не все наши компиляторы для поддерживаемых платформ полностью поддерживают C++11, поэтому мы не можем просто использовать поддержку vanilla C++11.
Одной из идей является следующая реализация универсального класса защиты, который может быть унаследован для предоставления универсального класса защиты и наследоваться от него для создания класса блокировки-защиты:
template<class T, void (T::*EnterFn)(), void (T::*ExitFn)()>
class Guard
{
public: // copy constructor deleting omitted for brevity
Guard( T *lock ) : m_lock(lock) { (m_lock->*EnterFn)(); }
~Guard(); { (m_lock->*ExitFn)(); }
private:
T *m_lock;
};
template<class T>
class LockGuard : public Guard<T, &T::Lock, &T::Unlock>
{
public:
LockGuard(const T* lock) : Guard<T, &T::Lock, &T::Unlock>(lock) {}
};
Другая школа мысли состоит в том, чтобы просто реализовать простую защиту:
template<class T>
class LockGuard {
T* m_lockable;
public:
LockGuard(const T* lockable) : m_lockable(lockable) { lockable->Lock(); }
~LockGuard() { m_lockable->Unlock(); }
};
Какую реализацию вы бы выбрали и почему? Каков наиболее подходящий C++(03, 11, 14, 17) способ его реализации? Есть ли какая-то внутренняя ценность иметь общий класс Guard, как описано выше?
2 ответа
Я отвечу на вопрос в вашем заголовке, так как на него никто не ответил.
Согласно документам,lock_guard
работает с любым типом, который "соответствует BasicLockable
требования ". BasicLockable требует только два метода,lock()
а также unlock()
.
Чтобы сделать lock_guard
работать с пользовательской библиотекой, вам нужно либо добавить lock()
а также unlock()
методы в класс мьютекса библиотеки или оберните его в другой класс, который имеет lock()
а также unlock()
методы.
Я не хотел бы использовать указатели метода.
Лично я бы хотел максимально приблизиться к стандартным инструментам C++11. Так что я бы написал адаптер.
template<class T>
struct lock_adapter {
T* t = nullptr;
void lock() { t->Lock(); }
void unlock() { t->Unlock(); }
lock_adapter( T& tin ):t(std::addressof(tin)) {}
// default some stuff if you like
};
template<class T>
struct adapted_unique_lock:
private lock_adapter<T>,
std::unique_lock< lock_adapter<T> >
{
template<class...Args>
adapted_unique_lock(T& t, Args&&...):
lock_adapter<T>(t),
std::unique_lock< lock_adapter<T> >( *this, std::forward<Args>(args)... )
{}
adapted_unique_lock(adapted_unique_lock&&)=delete; // sadly
friend void swap( adapted_unique_lock&, adapted_unique_lock& ) = delete; // ditto
};
сейчас adapted_unique_lock
имеет ограниченный набор функций от std::unique_lock
,
Он не может быть перемещен, так как unique_lock
содержит указатель на this
внутри его реализации и не переустанавливать его.
Обратите внимание, что богатство всего unique_lock
набор конструктора доступен.
Функции, которые возвращают адаптированные уникальные блокировки, должны хранить свои возвращаемые значения во что-то вроде auto&&
ссылки до конца области, и вы не можете вернуть их через цепочки до C++17.
Но любой код, использующий adapted_unique_lock
можно поменять местами для использования unique_lock
один раз T
был изменен на поддержку .lock()
а также .unlock()
, Это перемещает вашу кодовую базу в сторону того, чтобы быть более стандартным в C++11, а не на заказ.
Вы должны использовать их: std::mutex, std::shared_lock, std::unique_lock, std::timed_mutex, std::shared_mutex, std::recursive_mutex, std::shared_timed_mutex, std::recursive_timed_mutex, std::lock_guard if на самом деле у вас есть компилятор C++11. В противном случае это более сложно, и тег C++11 по вопросу должен быть удален.
Вы не можете реализовать спин-блокировку или мьютекс с помощью средств C\C++, вам потребуется добавить ассемблер или встроенный код - что делает его непереносимым, если вы не реализуете его для каждой платформы - и со многими компиляторами x86-64 C++11 и новее Вы не можете сделать встроенный ассемблер.
Основная проблема, с которой вы столкнетесь, если будете использовать внутреннюю логику блокировки, состоит в том, что объекты за мьютексами или спин-блокировками не копируются. Как только вы копируете его или копируете защищаемую переменную, он перестает быть заблокированным. Объекты, которые реализуют мьютексную механику, также не копируются.