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.
This commit is contained in:
Martchus 2022-12-03 23:53:23 +01:00
parent b1acbee127
commit c404a092cc
2 changed files with 101 additions and 4 deletions

View File

@ -3,6 +3,8 @@
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <list>
#include <mutex>
#include <shared_mutex>
@ -16,17 +18,23 @@ struct LogContext;
struct GlobalSharedMutex {
void lock();
bool try_lock();
void lock_async(std::function<void()> &&callback);
void unlock();
void lock_shared();
bool try_lock_shared();
void lock_shared_async(std::function<void()> &&callback);
void unlock_shared();
private:
void notify(std::unique_lock<std::mutex> &lock);
std::mutex m_mutex;
std::condition_variable m_cv;
std::uint32_t m_sharedOwners = 0;
bool m_exclusivelyOwned = false;
std::list<std::function<void()>> m_sharedCallbacks;
std::function<void()> m_exclusiveCallback;
};
inline void GlobalSharedMutex::lock()
@ -48,12 +56,23 @@ inline bool GlobalSharedMutex::try_lock()
}
}
inline void GlobalSharedMutex::lock_async(std::function<void()> &&callback)
{
auto lock = std::unique_lock<std::mutex>(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<std::mutex>(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<void()> &&callback)
{
auto lock = std::unique_lock<std::mutex>(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<std::mutex>(m_mutex);
if (!--m_sharedOwners) {
lock.unlock();
m_cv.notify_one();
notify(lock);
}
}
inline void GlobalSharedMutex::notify(std::unique_lock<std::mutex> &lock)
{
// invoke callbacks for lock_shared_async()
if (!m_sharedCallbacks.empty() && !m_exclusivelyOwned) {
auto callbacks = std::move(m_sharedCallbacks);
m_sharedOwners += static_cast<std::uint32_t>(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 <typename UnderlyingLockType> struct LoggingLock {
template <typename... Args> LoggingLock(LogContext &log, std::string &&name, Args &&...args);

View File

@ -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();