diff --git a/CMakeLists.txt b/CMakeLists.txt index 193e3d2..bba7f63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,7 @@ if(CPPDAP_BUILD_TESTS) ${CPPDAP_SRC_DIR}/json_serializer_test.cpp ${CPPDAP_SRC_DIR}/network_test.cpp ${CPPDAP_SRC_DIR}/optional_test.cpp + ${CPPDAP_SRC_DIR}/rwmutex_test.cpp ${CPPDAP_SRC_DIR}/session_test.cpp ${CPPDAP_SRC_DIR}/socket_test.cpp ${CPPDAP_SRC_DIR}/typeinfo_test.cpp diff --git a/src/rwmutex.h b/src/rwmutex.h new file mode 100644 index 0000000..9e85891 --- /dev/null +++ b/src/rwmutex.h @@ -0,0 +1,172 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef dap_rwmutex_h +#define dap_rwmutex_h + +#include +#include + +namespace dap { + +//////////////////////////////////////////////////////////////////////////////// +// RWMutex +//////////////////////////////////////////////////////////////////////////////// + +// A RWMutex is a reader/writer mutual exclusion lock. +// The lock can be held by an arbitrary number of readers or a single writer. +// Also known as a shared mutex. +class RWMutex { + public: + inline RWMutex() = default; + + // lockReader() locks the mutex for reading. + // Multiple read locks can be held while there are no writer locks. + inline void lockReader(); + + // unlockReader() unlocks the mutex for reading. + inline void unlockReader(); + + // lockWriter() locks the mutex for writing. + // If the lock is already locked for reading or writing, lockWriter blocks + // until the lock is available. + inline void lockWriter(); + + // unlockWriter() unlocks the mutex for writing. + inline void unlockWriter(); + + private: + RWMutex(const RWMutex&) = delete; + RWMutex& operator=(const RWMutex&) = delete; + + int readLocks = 0; + int pendingWriteLocks = 0; + std::mutex mutex; + std::condition_variable cv; +}; + +void RWMutex::lockReader() { + std::unique_lock lock(mutex); + readLocks++; +} + +void RWMutex::unlockReader() { + std::unique_lock lock(mutex); + readLocks--; + if (readLocks == 0 && pendingWriteLocks > 0) { + cv.notify_one(); + } +} + +void RWMutex::lockWriter() { + std::unique_lock lock(mutex); + if (readLocks > 0) { + pendingWriteLocks++; + cv.wait(lock, [&] { return readLocks == 0; }); + pendingWriteLocks--; + } + lock.release(); // Keep lock held +} + +void RWMutex::unlockWriter() { + if (pendingWriteLocks > 0) { + cv.notify_one(); + } + mutex.unlock(); +} + +//////////////////////////////////////////////////////////////////////////////// +// RLock +//////////////////////////////////////////////////////////////////////////////// + +// RLock is a RAII read lock helper for a RWMutex. +class RLock { + public: + inline RLock(RWMutex& mutex); + inline ~RLock(); + + inline RLock(RLock&&); + inline RLock& operator=(RLock&&); + + private: + RLock(const RLock&) = delete; + RLock& operator=(const RLock&) = delete; + + RWMutex* m; +}; + +RLock::RLock(RWMutex& mutex) : m(&mutex) { + m->lockReader(); +} + +RLock::~RLock() { + if (m != nullptr) { + m->unlockReader(); + } +} + +RLock::RLock(RLock&& other) { + m = other.m; + other.m = nullptr; +} + +RLock& RLock::operator=(RLock&& other) { + m = other.m; + other.m = nullptr; + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +// WLock +//////////////////////////////////////////////////////////////////////////////// + +// WLock is a RAII write lock helper for a RWMutex. +class WLock { + public: + inline WLock(RWMutex& mutex); + inline ~WLock(); + + inline WLock(WLock&&); + inline WLock& operator=(WLock&&); + + private: + WLock(const WLock&) = delete; + WLock& operator=(const WLock&) = delete; + + RWMutex* m; +}; + +WLock::WLock(RWMutex& mutex) : m(&mutex) { + m->lockWriter(); +} + +WLock::~WLock() { + if (m != nullptr) { + m->unlockWriter(); + } +} + +WLock::WLock(WLock&& other) { + m = other.m; + other.m = nullptr; +} + +WLock& WLock::operator=(WLock&& other) { + m = other.m; + other.m = nullptr; + return *this; +} +} // namespace dap + +#endif diff --git a/src/rwmutex_test.cpp b/src/rwmutex_test.cpp new file mode 100644 index 0000000..944ed77 --- /dev/null +++ b/src/rwmutex_test.cpp @@ -0,0 +1,113 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "rwmutex.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace { +constexpr const size_t NumThreads = 8; +} + +// Check that WLock behaves like regular mutex. +TEST(RWMutex, WLock) { + dap::RWMutex rwmutex; + int counter = 0; + + std::vector threads; + for (size_t i = 0; i < NumThreads; i++) { + threads.emplace_back([&] { + for (int j = 0; j < 1000; j++) { + dap::WLock lock(rwmutex); + counter++; + EXPECT_EQ(counter, 1); + counter--; + } + }); + } + + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_EQ(counter, 0); +} + +TEST(RWMutex, NoRLockWithWLock) { + dap::RWMutex rwmutex; + + std::vector threads; + std::array counters = {}; + + { // With WLock held... + dap::WLock wlock(rwmutex); + + for (size_t i = 0; i < counters.size(); i++) { + int* counter = &counters[i]; + threads.emplace_back([&rwmutex, counter] { + dap::RLock lock(rwmutex); + for (int j = 0; j < 1000; j++) { + (*counter)++; + } + }); + } + + // RLocks should block + for (int counter : counters) { + EXPECT_EQ(counter, 0); + } + } + + for (auto& thread : threads) { + thread.join(); + } + + for (int counter : counters) { + EXPECT_EQ(counter, 1000); + } +} + +TEST(RWMutex, NoWLockWithRLock) { + dap::RWMutex rwmutex; + + std::vector threads; + size_t counter = 0; + + { // With RLocks held... + dap::RLock rlockA(rwmutex); + dap::RLock rlockB(rwmutex); + dap::RLock rlockC(rwmutex); + + for (size_t i = 0; i < NumThreads; i++) { + threads.emplace_back(std::thread([&] { + dap::WLock lock(rwmutex); + counter++; + })); + } + + // ... WLocks should block + EXPECT_EQ(counter, 0U); + } + + for (auto& thread : threads) { + thread.join(); + } + + EXPECT_EQ(counter, NumThreads); +}