// Copyright (C) 2020 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. #include #include #include #include #include #include #include #include #include using namespace isc::util; using namespace std; namespace { /// @brief Test Fixture for testing read-write mutexes. /// /// Each not basic test follows the same schema: /// @code /// main thread work thread /// <- started /// work -> /// enter guard /// <- done /// terminate -> /// return /// join /// @endcode class ReadWriteMutexTest : public ::testing::Test { public: /// @brief Read-write mutex. ReadWriteMutex rw_mutex_; /// @brief Synchronization objects for work threads. struct Sync { bool started = false; mutex started_mtx; condition_variable started_cv; bool work = false; mutex work_mtx; condition_variable work_cv; bool done = false; mutex done_mtx; condition_variable done_cv; bool terminate = false; mutex terminate_mtx; condition_variable terminate_cv; } syncr_, syncw_; /// @brief Body of the reader. /// /// @param rw_mutex The read-write mutex. /// @param syncr The reader synchronization object. void reader(ReadWriteMutex& rw_mutex, Sync& syncr) { // Take mutex to wait for main thread signals. unique_lock terminate_lock(syncr.terminate_mtx); // Signal the thread started. { lock_guard lock(syncr.started_mtx); syncr.started = true; } // Wait for work. { unique_lock work_lock(syncr.work_mtx); // When this thread starts waiting, the main thread is resumed. syncr.started_cv.notify_one(); syncr.work_cv.wait(work_lock, [&](){ return syncr.work; }); } { // Enter a read lock guard. ReadLockGuard rwlock(rw_mutex); // Signal the thread holds the guard. { lock_guard done_lock(syncr.done_mtx); syncr.done = true; } syncr.done_cv.notify_one(); } // Wait to terminate. syncr.terminate_cv.wait(terminate_lock, [&](){ return syncr.terminate; }); } /// @brief Body of the writer. /// /// @param rw_mutex The read-write mutex. /// @param syncw The writer synchronization object. void writer(ReadWriteMutex& rw_mutex, Sync& syncw) { // Take mutex to wait for main thread signals. unique_lock terminate_lock(syncw.terminate_mtx); // Signal the thread started. { lock_guard lock(syncw.started_mtx); syncw.started = true; } // Wait for work. { unique_lock work_lock(syncw.work_mtx); // When this thread starts waiting, the main thread is resumed. syncw.started_cv.notify_one(); syncw.work_cv.wait(work_lock, [&](){ return syncw.work; }); } { // Enter a write lock guard. WriteLockGuard rwlock(rw_mutex); // Signal the thread holds the guard. { lock_guard done_lock(syncw.done_mtx); syncw.done = true; } syncw.done_cv.notify_one(); } // Wait to terminate. syncw.terminate_cv.wait(terminate_lock, [&](){ return syncw.terminate; }); } }; // Verify basic read lock guard. TEST_F(ReadWriteMutexTest, basicRead) { ReadLockGuard lock(rw_mutex_); } // Verify basic write lock guard. TEST_F(ReadWriteMutexTest, basicWrite) { WriteLockGuard lock(rw_mutex_); } // Verify read lock guard using a thread. TEST_F(ReadWriteMutexTest, read) { // Take mutex to wait for work thread signals. boost::shared_ptr thread; { unique_lock started_lock(syncr_.started_mtx); // Create a work thread. thread = boost::make_shared([this](){ reader(rw_mutex_, syncr_); }); // Wait work thread to start. syncr_.started_cv.wait(started_lock, [this](){ return syncr_.started; }); unique_lock done_lock(syncr_.done_mtx); // Signal the thread to work. { lock_guard work_lock(syncr_.work_mtx); syncr_.work = true; } syncr_.work_cv.notify_one(); // Wait thread to hold the read lock. syncr_.done_cv.wait(done_lock, [this](){ return syncr_.done; }); } // Signal the thread to terminate. { lock_guard terminate_lock(syncr_.terminate_mtx); syncr_.terminate = true; } syncr_.terminate_cv.notify_one(); // Join the thread. thread->join(); } // Verify write lock guard using a thread. TEST_F(ReadWriteMutexTest, write) { // Take mutex to wait for work thread signals. boost::shared_ptr thread; { unique_lock started_lock(syncw_.started_mtx); // Create a work thread. thread = boost::make_shared([this](){ writer(rw_mutex_, syncw_); }); // Wait work thread to start. syncw_.started_cv.wait(started_lock, [this](){ return syncw_.started; }); unique_lock done_lock(syncw_.done_mtx); // Signal the thread to work. { lock_guard work_lock(syncw_.work_mtx); syncw_.work = true; } syncw_.work_cv.notify_one(); // Wait thread to hold the write lock. syncw_.done_cv.wait(done_lock, [this](){ return syncw_.done; }); } // Signal the thread to terminate. { lock_guard terminate_lock(syncw_.terminate_mtx); syncw_.terminate = true; } syncw_.terminate_cv.notify_one(); // Join the thread. thread->join(); } // Verify read lock guard can be acquired by multiple threads. TEST_F(ReadWriteMutexTest, readRead) { // Take mutex to wait for work thread signals. boost::shared_ptr thread; { unique_lock started_lock(syncr_.started_mtx); // Create a work thread. thread = boost::make_shared([this](){ reader(rw_mutex_, syncr_); }); // Enter a read lock guard. ReadLockGuard rwlock(rw_mutex_); // Wait work thread to start. syncr_.started_cv.wait(started_lock, [this](){ return syncr_.started; }); unique_lock done_lock(syncr_.done_mtx); // Signal the thread to work. { lock_guard work_lock(syncr_.work_mtx); syncr_.work = true; } syncr_.work_cv.notify_one(); // Wait thread to hold the read lock. syncr_.done_cv.wait(done_lock, [this](){ return syncr_.done; }); } // Signal the thread to terminate. { lock_guard terminate_lock(syncr_.terminate_mtx); syncr_.terminate = true; } syncr_.terminate_cv.notify_one(); // Join the thread. thread->join(); } // Verify write lock guard is exclusive of a reader. TEST_F(ReadWriteMutexTest, readWrite) { // Take mutex to wait for work thread signals. boost::shared_ptr thread; { unique_lock started_lock(syncw_.started_mtx); // Create a work thread. thread = boost::make_shared([this](){ writer(rw_mutex_, syncw_); }); // Wait work thread to start. syncw_.started_cv.wait(started_lock, [this](){ return syncw_.started; }); unique_lock done_lock(syncw_.done_mtx); { // Enter a read lock guard. ReadLockGuard rwlock(rw_mutex_); // Signal the thread to work. { lock_guard work_lock(syncw_.work_mtx); syncw_.work = true; } syncw_.work_cv.notify_one(); // Verify the work thread is waiting for the write lock. cout << "pausing for one second" << std::endl; bool ret = syncw_.done_cv.wait_for(done_lock, chrono::seconds(1), [this](){ return syncw_.done; }); EXPECT_FALSE(syncw_.done); EXPECT_FALSE(ret); // Exiting the read lock guard. } // Wait thread to hold the write lock. syncw_.done_cv.wait(done_lock, [this](){ return syncw_.done; }); } // Signal the thread to terminate. { lock_guard terminate_lock(syncw_.terminate_mtx); syncw_.terminate = true; } syncw_.terminate_cv.notify_one(); // Join the thread. thread->join(); } // Verify write lock guard is exclusive of a writer. TEST_F(ReadWriteMutexTest, writeWrite) { // Take mutex to wait for work thread signals. boost::shared_ptr thread; { unique_lock started_lock(syncw_.started_mtx); // Create a work thread. thread = boost::make_shared([this](){ writer(rw_mutex_, syncw_); }); // Wait work thread to start. syncw_.started_cv.wait(started_lock, [this](){ return syncw_.started; }); unique_lock done_lock(syncw_.done_mtx); { // Enter a write lock guard. WriteLockGuard rwlock(rw_mutex_); // Signal the thread to work. { lock_guard work_lock(syncw_.work_mtx); syncw_.work = true; } syncw_.work_cv.notify_one(); // Verify the work thread is waiting for the write lock. cout << "pausing for one second" << std::endl; bool ret = syncw_.done_cv.wait_for(done_lock, chrono::seconds(1), [this](){ return syncw_.done; }); EXPECT_FALSE(syncw_.done); EXPECT_FALSE(ret); // Exiting the write lock guard. } // Wait thread to hold the write lock. syncw_.done_cv.wait(done_lock, [this](){ return syncw_.done; }); } // Signal the thread to terminate. { lock_guard terminate_lock(syncw_.terminate_mtx); syncw_.terminate = true; } syncw_.terminate_cv.notify_one(); // Join the thread. thread->join(); } // Verify that a writer has the preference. TEST_F(ReadWriteMutexTest, readWriteRead) { // Take mutex to wait for work thread signals. boost::shared_ptr threadw; { unique_lock startedw_lock(syncw_.started_mtx); // First thread is a writer. threadw = boost::make_shared([this](){ writer(rw_mutex_, syncw_); }); // Wait work thread to start. syncw_.started_cv.wait(startedw_lock, [this](){ return syncw_.started; }); } boost::shared_ptr threadr; { unique_lock startedr_lock(syncr_.started_mtx); // Second thread is a reader. threadr = boost::make_shared([this](){ reader(rw_mutex_, syncr_); }); // Wait work thread to start. syncr_.started_cv.wait(startedr_lock, [this](){ return syncr_.started; }); } { unique_lock donew_lock(syncw_.done_mtx); { // Enter a read lock guard. ReadLockGuard rwlock(rw_mutex_); // Signal the writer thread to work. { lock_guard work_lock(syncw_.work_mtx); syncw_.work = true; } syncw_.work_cv.notify_one(); // Verify the writer thread is waiting for the write lock. cout << "pausing for one second" << std::endl; bool ret = syncw_.done_cv.wait_for(donew_lock, chrono::seconds(1), [this](){ return syncw_.done; }); EXPECT_FALSE(syncw_.done); EXPECT_FALSE(ret); { unique_lock doner_lock(syncr_.done_mtx); // Signal the reader thread to work. { lock_guard work_lock(syncr_.work_mtx); syncr_.work = true; } syncr_.work_cv.notify_one(); // Verify the reader thread is waiting for the read lock. cout << "pausing for one second" << std::endl; bool ret = syncr_.done_cv.wait_for(doner_lock, chrono::seconds(1), [this](){ return syncr_.done; }); EXPECT_FALSE(syncr_.done); EXPECT_FALSE(ret); } // Exiting the read lock guard. } { unique_lock doner_lock(syncr_.done_mtx); // Verify the reader thread is still waiting for the read lock. cout << "pausing for one second" << std::endl; bool ret = syncr_.done_cv.wait_for(doner_lock, chrono::seconds(1), [this](){ return syncr_.done; }); EXPECT_FALSE(syncr_.done); EXPECT_FALSE(ret); } // Wait writer thread to hold the write lock. syncw_.done_cv.wait(donew_lock, [this](){ return syncw_.done; }); } { unique_lock doner_lock(syncr_.done_mtx); // Wait reader thread to hold the read lock. syncr_.done_cv.wait(doner_lock, [this](){ return syncr_.done; }); } // Signal the writer thread to terminate. { lock_guard terminate_lock(syncw_.terminate_mtx); syncw_.terminate = true; } syncw_.terminate_cv.notify_one(); // Join the writer thread. threadw->join(); // Signal the reader thread to terminate. { lock_guard terminate_lock(syncr_.terminate_mtx); syncr_.terminate = true; } syncr_.terminate_cv.notify_one(); // Join the reader thread. threadr->join(); } }