From c404a092cc5ac6dc3d129bffe256623b90bea010 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 3 Dec 2022 23:53:23 +0100 Subject: [PATCH] Add async locks This might be useful to avoid blocking threads in the thread pool just for waiting on a global lock. It might also be useful to allow stopping build actions while they're waiting for a lock. --- librepomgr/globallock.h | 65 +++++++++++++++++++++++++++++++++++--- librepomgr/tests/utils.cpp | 40 +++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/librepomgr/globallock.h b/librepomgr/globallock.h index 32456fe..64da977 100644 --- a/librepomgr/globallock.h +++ b/librepomgr/globallock.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include @@ -16,17 +18,23 @@ struct LogContext; struct GlobalSharedMutex { void lock(); bool try_lock(); + void lock_async(std::function &&callback); void unlock(); void lock_shared(); bool try_lock_shared(); + void lock_shared_async(std::function &&callback); void unlock_shared(); private: + void notify(std::unique_lock &lock); + std::mutex m_mutex; std::condition_variable m_cv; std::uint32_t m_sharedOwners = 0; bool m_exclusivelyOwned = false; + std::list> m_sharedCallbacks; + std::function m_exclusiveCallback; }; inline void GlobalSharedMutex::lock() @@ -48,12 +56,23 @@ inline bool GlobalSharedMutex::try_lock() } } +inline void GlobalSharedMutex::lock_async(std::function &&callback) +{ + auto lock = std::unique_lock(m_mutex); + if (m_sharedOwners || m_exclusivelyOwned) { + m_exclusiveCallback = std::move(callback); + } else { + m_exclusivelyOwned = true; + lock.unlock(); + callback(); + } +} + inline void GlobalSharedMutex::unlock() { auto lock = std::unique_lock(m_mutex); m_exclusivelyOwned = false; - lock.unlock(); - m_cv.notify_one(); + notify(lock); } inline void GlobalSharedMutex::lock_shared() @@ -75,15 +94,53 @@ inline bool GlobalSharedMutex::try_lock_shared() } } +inline void GlobalSharedMutex::lock_shared_async(std::function &&callback) +{ + auto lock = std::unique_lock(m_mutex); + if (m_exclusivelyOwned) { + m_sharedCallbacks.emplace_back(std::move(callback)); + } else { + ++m_sharedOwners; + lock.unlock(); + callback(); + } +} + inline void GlobalSharedMutex::unlock_shared() { auto lock = std::unique_lock(m_mutex); if (!--m_sharedOwners) { - lock.unlock(); - m_cv.notify_one(); + notify(lock); } } +inline void GlobalSharedMutex::notify(std::unique_lock &lock) +{ + // invoke callbacks for lock_shared_async() + if (!m_sharedCallbacks.empty() && !m_exclusivelyOwned) { + auto callbacks = std::move(m_sharedCallbacks); + m_sharedOwners += static_cast(callbacks.size()); + lock.unlock(); + for (auto &callback : callbacks) { + callback(); + } + return; + } + // invoke callbacks for lock_async() + if (m_exclusiveCallback) { + if (!m_sharedOwners && !m_exclusivelyOwned) { + auto callback = std::move(m_exclusiveCallback); + m_exclusivelyOwned = true; + lock.unlock(); + callback(); + return; + } + } + // resume threads blocked in lock() and lock_shared() + lock.unlock(); + m_cv.notify_one(); +} + /// \brief A wrapper around a standard lock which logs acquisition/release. template struct LoggingLock { template LoggingLock(LogContext &log, std::string &&name, Args &&...args); diff --git a/librepomgr/tests/utils.cpp b/librepomgr/tests/utils.cpp index a1626ee..977b4ed 100644 --- a/librepomgr/tests/utils.cpp +++ b/librepomgr/tests/utils.cpp @@ -21,10 +21,12 @@ using namespace LibRepoMgr; class UtilsTests : public TestFixture { CPPUNIT_TEST_SUITE(UtilsTests); CPPUNIT_TEST(testGlobalLock); + CPPUNIT_TEST(testGlobalLockAsync); CPPUNIT_TEST(testLockTable); CPPUNIT_TEST_SUITE_END(); void testGlobalLock(); + void testGlobalLockAsync(); void testLockTable(); public: @@ -73,6 +75,44 @@ void UtilsTests::testGlobalLock() mutex.unlock(); } +void UtilsTests::testGlobalLockAsync() +{ + auto mutex = GlobalSharedMutex(); + auto sharedLock1 = false, sharedLock2 = false; + mutex.lock_shared_async([&sharedLock1] { sharedLock1 = true; }); + CPPUNIT_ASSERT(sharedLock1); + mutex.lock_shared_async([&sharedLock2] { sharedLock2 = true; }); // locking twice is not a problem, also not from the same thread + CPPUNIT_ASSERT(sharedLock2); + auto thread1 = std::thread([&mutex] { + mutex.unlock_shared(); // unlocking from another thread is ok + }); + auto lock1 = false, lock2 = false; + auto thread2 = std::thread([&mutex, &lock2] { + mutex.lock(); + lock2 = true; + }); + mutex.lock_async([&lock1] { lock1 = true; }); + CPPUNIT_ASSERT_MESSAGE("lock_async() not yet invoked", !lock1); + CPPUNIT_ASSERT_MESSAGE("blocking lock() not yet invoked", !lock2); + thread1.join(); + mutex.unlock_shared(); + CPPUNIT_ASSERT_MESSAGE("lock_async() callback invoked via unlock_shared()", lock1); + CPPUNIT_ASSERT_MESSAGE("blocking lock() not yet invoked (async callbacks are handled first)", !lock2); + mutex.unlock(); // release async lock so … + thread2.join(); // … thread2 is able to acquire the mutex exclusively (and then terminate) + CPPUNIT_ASSERT_MESSAGE("try_lock_shared() returns false if mutex exclusively locked", !mutex.try_lock_shared()); + auto sharedLock3 = false; + mutex.lock_shared_async([&sharedLock3] { sharedLock3 = true; }); + mutex.unlock(); + CPPUNIT_ASSERT_MESSAGE("lock_async() callback invoked via unlock()", lock1); + CPPUNIT_ASSERT_MESSAGE("try_lock_shared() possible if mutex only shared locked", mutex.try_lock_shared()); + mutex.unlock_shared(); + CPPUNIT_ASSERT_MESSAGE("try_lock() returns false if mutex has still shared locked", !mutex.try_lock()); + mutex.unlock_shared(); + CPPUNIT_ASSERT_MESSAGE("try_lock() possible if mutex not locked", mutex.try_lock()); + mutex.unlock(); +} + void UtilsTests::testLockTable() { auto log = LogContext();