#ifndef OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H #define OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H // std #include #include #include #include // mlogic::common #include "scopeguard.h" namespace osdev { namespace components { namespace mqtt { #define OSDEV_COMPONENTS_SHAREDLOCK_SCOPE(lockvar) \ lockvar.lockShared(); \ OSDEV_COMPONENTS_SCOPEGUARD(lockvar, [&]() { lockvar.unlock(); }); #define OSDEV_COMPONENTS_EXCLUSIVELOCK_SCOPE(lockvar) \ lockvar.lockExclusive(); \ OSDEV_COMPONENTS_SCOPEGUARD(lockvar, [&]() { lockvar.unlock(); }); /** * @brief Class is used to administrate the lock data. */ class LockData { public: /** * @brief Default constructable. Lock is not active and the count is 0. */ LockData() : m_count(0) , m_active(false) { } // Copyable, movable LockData(const LockData&) = default; LockData& operator=(const LockData&) = default; LockData(LockData&&) = default; LockData& operator=(LockData&&) = default; /** * @return true when the lock is active, false otherwise. * @note A lock becomes active the first time that increase() is called. */ inline bool active() const { return m_active; } /** * @brief Increases the lock count by one. * An inactive lock becomes active by this call. */ inline void increase() { m_active = true; ++m_count; } /** * @brief Decreases the lock count by one. * The count is only decreased for active locks. When the lock count becomes 0 the lock * is deactivated. * @return true when the lock is still active after decrease and false when it is deactivated. */ inline bool decrease() { if (m_active) { --m_count; m_active = (0 != m_count); } return m_active; } /** * @brief Conversion operator that returns the lock count. */ inline operator std::size_t() const { return m_count; } /** * @brief Static method for initializing a lock data based on already existing lock data. * The new lock data is not active. * @note This is used to promote a shared lock to an exclusive lock. */ inline static LockData initialize(const LockData& other) { auto newLockData(other); newLockData.m_active = false; return newLockData; } private: std::size_t m_count; ///< The lock count. /** * @brief Flag to indicate whether the lock is active. * This flag is necessary because when the lock is promoted * the lock count is not zero but the lock still should be activated again. */ bool m_active; }; /** * @brief Lock class that allows multiple readers to own the lock in a shared way. * A writer will want exclusive ownership so that it can mutate the content that * is protected by this lock. * * Reader and writer should be interpreted as to how threads interact with the content that this lock protects. It is up * to the caller to enforce the correct behaviour. In other words don't take a shared lock and change the content! * * The administration of this class uses the std::thread::id to register which thread holds what kind of lock. * This id is reused, so be really careful to pair each lock with an unlock, otherwise newly spawned threads might * end up having a lock without taking one. */ class SharedReaderLock { public: /** * Default constructable. * The lock is not locked. */ SharedReaderLock(); /** * Destructor will throw when there are threads registered. */ ~SharedReaderLock(); // Non copyable, non movable SharedReaderLock(const SharedReaderLock&) = delete; SharedReaderLock& operator=(const SharedReaderLock&) = delete; SharedReaderLock(SharedReaderLock&&) = delete; SharedReaderLock& operator=(SharedReaderLock&&) = delete; /** * @brief Lock in a shared way. For read only operations. * Multiple threads can have shared ownership on this lock. * It is guaranteed that a call to lockExclusive will wait until all read locks are unlocked. * When a call to lockExclusive is made and is waiting, no new reader locks are accepted. * A thread that owns a shared lock can lock again. The lock will be unlocked for this thread when as many unlock calls are made. * A thread that owns a shared lock can upgrade the lock to an exclusive lock by calling lockExclusive. The thread has to wait * for exclusive ownership and has the exclusive lock until all unlocks are made (if it had done multiple shared locks before an exclusive lock). */ void lockShared(); /** * @brief Lock in an exclusive way. For write operations. * Only one thread can have exclusive ownership of this lock. * While a thread waits for exlusive ownership shared locks are denied. This lock is unfair in the * sense that it favours write locks. * A thread that owns exclusive ownership can make another exclusive lock or a even a shared lock. Both are * treated as an exclusive lock that updates the lock count. As many unlocks need to be called to unlock the exclusive lock. */ void lockExclusive(); /** * @brief Unlock the lock. The thread id is used to determine which lock needs to be unlocked. * If a thread does not own this lock at all then nothing happens. */ void unlock(); private: std::mutex m_mutex; ///< Mutex that protects the lock administration. std::map m_readLockMap; ///< Map with read lock data. std::map m_writeLockMap; ///< Map with write lock data. std::condition_variable m_readersCV; ///< lockShared waits on this condition variable. std::condition_variable m_writersCV; ///< lockExclusive waits on this condition variable. }; } // End namespace mqtt } // End namespace components } // End namespace osdev #endif // OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H