sharedreaderlock.h 7.7 KB
/* ****************************************************************************
 * Copyright 2019 Open Systems Development BV                                 *
 *                                                                            *
 * Permission is hereby granted, free of charge, to any person obtaining a    *
 * copy of this software and associated documentation files (the "Software"), *
 * to deal in the Software without restriction, including without limitation  *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,   *
 * and/or sell copies of the Software, and to permit persons to whom the      *
 * Software is furnished to do so, subject to the following conditions:       *
 *                                                                            *
 * The above copyright notice and this permission notice shall be included in *
 * all copies or substantial portions of the Software.                        *
 *                                                                            *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    *
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        *
 * DEALINGS IN THE SOFTWARE.                                                  *
 * ***************************************************************************/
#ifndef OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H
#define OSDEV_COMPONENTS_MQTT_SHAREDREADERLOCK_H

// std
#include <condition_variable>
#include <map>
#include <mutex>
#include <thread>

// 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<std::thread::id, LockData> m_readLockMap;  ///< Map with read lock data.
    std::map<std::thread::id, LockData> 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