lib/core/unittest/CReadWriteLockTest.cc (326 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the following additional limitation. Functionality enabled by the
* files subject to the Elastic License 2.0 may only be used in production when
* invoked by an Elasticsearch process with a license key installed that permits
* use of machine learning features. You may not use this file except in
* compliance with the Elastic License 2.0 and the foregoing additional
* limitation.
*/
#include <core/CFastMutex.h>
#include <core/CLogger.h>
#include <core/CMutex.h>
#include <core/CReadWriteLock.h>
#include <core/CScopedFastLock.h>
#include <core/CScopedLock.h>
#include <core/CScopedReadLock.h>
#include <core/CScopedWriteLock.h>
#include <core/CThread.h>
#include <core/CTimeUtils.h>
#include <boost/test/unit_test.hpp>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <thread>
BOOST_AUTO_TEST_SUITE(CReadWriteLockTest)
namespace {
class CUnprotectedAdder : public ml::core::CThread {
public:
CUnprotectedAdder(std::uint32_t sleepTime,
std::uint32_t iterations,
std::uint32_t increment,
volatile std::uint32_t& variable)
: m_SleepTime(sleepTime), m_Iterations(iterations),
m_Increment(increment), m_Variable(variable) {}
protected:
void run() override {
for (std::uint32_t count = 0; count < m_Iterations; ++count) {
m_Variable += m_Increment;
std::this_thread::sleep_for(std::chrono::milliseconds(m_SleepTime));
}
}
void shutdown() override {
// Always just wait for run() to complete
}
private:
std::uint32_t m_SleepTime;
std::uint32_t m_Iterations;
std::uint32_t m_Increment;
volatile std::uint32_t& m_Variable;
};
class CAtomicAdder : public ml::core::CThread {
public:
CAtomicAdder(std::uint32_t sleepTime,
std::uint32_t iterations,
std::uint32_t increment,
std::atomic_uint_fast32_t& variable)
: m_SleepTime(sleepTime), m_Iterations(iterations),
m_Increment(increment), m_Variable(variable) {}
protected:
void run() override {
for (std::uint32_t count = 0; count < m_Iterations; ++count) {
m_Variable.fetch_add(m_Increment);
std::this_thread::sleep_for(std::chrono::milliseconds(m_SleepTime));
}
}
void shutdown() override {
// Always just wait for run() to complete
}
private:
std::uint32_t m_SleepTime;
std::uint32_t m_Iterations;
std::uint32_t m_Increment;
std::atomic_uint_fast32_t& m_Variable;
};
class CFastMutexProtectedAdder : public ml::core::CThread {
public:
CFastMutexProtectedAdder(ml::core::CFastMutex& mutex,
std::uint32_t sleepTime,
std::uint32_t iterations,
std::uint32_t increment,
volatile std::uint32_t& variable)
: m_Mutex(mutex), m_SleepTime(sleepTime), m_Iterations(iterations),
m_Increment(increment), m_Variable(variable) {}
protected:
void run() override {
for (std::uint32_t count = 0; count < m_Iterations; ++count) {
ml::core::CScopedFastLock lock(m_Mutex);
m_Variable += m_Increment;
std::this_thread::sleep_for(std::chrono::milliseconds(m_SleepTime));
}
}
void shutdown() override {
// Always just wait for run() to complete
}
private:
ml::core::CFastMutex& m_Mutex;
std::uint32_t m_SleepTime;
std::uint32_t m_Iterations;
std::uint32_t m_Increment;
volatile std::uint32_t& m_Variable;
};
class CMutexProtectedAdder : public ml::core::CThread {
public:
CMutexProtectedAdder(ml::core::CMutex& mutex,
std::uint32_t sleepTime,
std::uint32_t iterations,
std::uint32_t increment,
volatile std::uint32_t& variable)
: m_Mutex(mutex), m_SleepTime(sleepTime), m_Iterations(iterations),
m_Increment(increment), m_Variable(variable) {}
protected:
void run() override {
for (std::uint32_t count = 0; count < m_Iterations; ++count) {
ml::core::CScopedLock lock(m_Mutex);
m_Variable += m_Increment;
std::this_thread::sleep_for(std::chrono::milliseconds(m_SleepTime));
}
}
void shutdown() override {
// Always just wait for run() to complete
}
private:
ml::core::CMutex& m_Mutex;
std::uint32_t m_SleepTime;
std::uint32_t m_Iterations;
std::uint32_t m_Increment;
volatile std::uint32_t& m_Variable;
};
class CWriteLockProtectedAdder : public ml::core::CThread {
public:
CWriteLockProtectedAdder(ml::core::CReadWriteLock& readWriteLock,
std::uint32_t sleepTime,
std::uint32_t iterations,
std::uint32_t increment,
volatile std::uint32_t& variable)
: m_ReadWriteLock(readWriteLock), m_SleepTime(sleepTime),
m_Iterations(iterations), m_Increment(increment), m_Variable(variable) {}
protected:
void run() override {
for (std::uint32_t count = 0; count < m_Iterations; ++count) {
ml::core::CScopedWriteLock lock(m_ReadWriteLock);
m_Variable += m_Increment;
std::this_thread::sleep_for(std::chrono::milliseconds(m_SleepTime));
}
}
void shutdown() override {
// Always just wait for run() to complete
}
private:
ml::core::CReadWriteLock& m_ReadWriteLock;
std::uint32_t m_SleepTime;
std::uint32_t m_Iterations;
std::uint32_t m_Increment;
volatile std::uint32_t& m_Variable;
};
class CReadLockProtectedReader : public ml::core::CThread {
public:
CReadLockProtectedReader(ml::core::CReadWriteLock& readWriteLock,
std::uint32_t sleepTime,
std::uint32_t iterations,
volatile std::uint32_t& variable)
: m_ReadWriteLock(readWriteLock), m_SleepTime(sleepTime),
m_Iterations(iterations), m_Variable(variable), m_LastRead(variable) {}
std::uint32_t lastRead() const { return m_LastRead; }
protected:
void run() override {
for (std::uint32_t count = 0; count < m_Iterations; ++count) {
ml::core::CScopedReadLock lock(m_ReadWriteLock);
m_LastRead = m_Variable;
std::this_thread::sleep_for(std::chrono::milliseconds(m_SleepTime));
}
}
void shutdown() override {
// Always just wait for run() to complete
}
private:
ml::core::CReadWriteLock& m_ReadWriteLock;
std::uint32_t m_SleepTime;
std::uint32_t m_Iterations;
volatile std::uint32_t& m_Variable;
std::uint32_t m_LastRead;
};
}
BOOST_AUTO_TEST_CASE(testReadLock) {
std::uint32_t testVariable(0);
ml::core::CReadWriteLock readWriteLock;
// Each reader will do 1 second of "work" inside a read lock. If they all
// work at the same time (which they should) then the test will take around
// 1 second. If they block each other, the test will take around 3
// seconds.
CReadLockProtectedReader reader1(readWriteLock, 100, 10, testVariable);
CReadLockProtectedReader reader2(readWriteLock, 100, 10, testVariable);
CReadLockProtectedReader reader3(readWriteLock, 100, 10, testVariable);
ml::core_t::TTime start(ml::core::CTimeUtils::now());
reader1.start();
reader2.start();
reader3.start();
testVariable = 42;
reader1.stop();
reader2.stop();
reader3.stop();
ml::core_t::TTime end(ml::core::CTimeUtils::now());
ml::core_t::TTime duration(end - start);
LOG_INFO(<< "Reader concurrency test took " << duration << " seconds");
// Allow the test to run up to 3 seconds, as there is processing
// other than the sleeping, and also sleeps are not very accurate
// under Jenkins on Apple M1.
BOOST_TEST_REQUIRE(duration <= 3);
BOOST_TEST_REQUIRE(duration >= 1);
BOOST_REQUIRE_EQUAL(testVariable, reader1.lastRead());
BOOST_REQUIRE_EQUAL(testVariable, reader2.lastRead());
BOOST_REQUIRE_EQUAL(testVariable, reader3.lastRead());
}
BOOST_AUTO_TEST_CASE(testWriteLock) {
static const std::uint32_t TEST_SIZE(50000);
std::uint32_t testVariable(0);
ml::core::CReadWriteLock readWriteLock;
CWriteLockProtectedAdder writer1(readWriteLock, 0, TEST_SIZE, 1, testVariable);
CWriteLockProtectedAdder writer2(readWriteLock, 0, TEST_SIZE, 5, testVariable);
CWriteLockProtectedAdder writer3(readWriteLock, 0, TEST_SIZE, 9, testVariable);
writer1.start();
writer2.start();
writer3.start();
writer1.stop();
writer2.stop();
writer3.stop();
LOG_INFO(<< "Write lock protected variable incremented to " << testVariable);
BOOST_REQUIRE_EQUAL(TEST_SIZE * (1 + 5 + 9), testVariable);
}
BOOST_AUTO_TEST_CASE(testPerformanceVersusMutex) {
static const std::uint32_t TEST_SIZE(1000000);
{
std::uint32_t testVariable(0);
ml::core_t::TTime start(ml::core::CTimeUtils::now());
LOG_INFO(<< "Starting unlocked throughput test at "
<< ml::core::CTimeUtils::toTimeString(start));
CUnprotectedAdder writer1(0, TEST_SIZE, 1, testVariable);
CUnprotectedAdder writer2(0, TEST_SIZE, 5, testVariable);
CUnprotectedAdder writer3(0, TEST_SIZE, 9, testVariable);
writer1.start();
writer2.start();
writer3.start();
writer1.stop();
writer2.stop();
writer3.stop();
ml::core_t::TTime end(ml::core::CTimeUtils::now());
LOG_INFO(<< "Finished unlocked throughput test at "
<< ml::core::CTimeUtils::toTimeString(end));
LOG_INFO(<< "Unlocked throughput test with test size " << TEST_SIZE
<< " took " << (end - start) << " seconds");
LOG_INFO(<< "Unlocked variable incremented to " << testVariable);
if (testVariable != TEST_SIZE * (1 + 5 + 9)) {
// Obviously this would be unacceptable in production code, but this
// unit test is showing the cost of different types of lock compared
// to the unlocked case
LOG_INFO(<< "Lack of locking caused race condition");
}
}
{
std::atomic_uint_fast32_t testVariable(0);
ml::core_t::TTime start(ml::core::CTimeUtils::now());
LOG_INFO(<< "Starting atomic throughput test at "
<< ml::core::CTimeUtils::toTimeString(start));
CAtomicAdder writer1(0, TEST_SIZE, 1, testVariable);
CAtomicAdder writer2(0, TEST_SIZE, 5, testVariable);
CAtomicAdder writer3(0, TEST_SIZE, 9, testVariable);
writer1.start();
writer2.start();
writer3.start();
writer1.stop();
writer2.stop();
writer3.stop();
ml::core_t::TTime end(ml::core::CTimeUtils::now());
LOG_INFO(<< "Finished atomic throughput test at "
<< ml::core::CTimeUtils::toTimeString(end));
LOG_INFO(<< "Atomic throughput test with test size " << TEST_SIZE
<< " took " << (end - start) << " seconds");
LOG_INFO(<< "Atomic variable incremented to " << testVariable.load());
BOOST_REQUIRE_EQUAL(uint_fast32_t(TEST_SIZE * (1 + 5 + 9)), testVariable.load());
}
{
std::uint32_t testVariable(0);
ml::core::CFastMutex mutex;
ml::core_t::TTime start(ml::core::CTimeUtils::now());
LOG_INFO(<< "Starting fast mutex lock throughput test at "
<< ml::core::CTimeUtils::toTimeString(start));
CFastMutexProtectedAdder writer1(mutex, 0, TEST_SIZE, 1, testVariable);
CFastMutexProtectedAdder writer2(mutex, 0, TEST_SIZE, 5, testVariable);
CFastMutexProtectedAdder writer3(mutex, 0, TEST_SIZE, 9, testVariable);
writer1.start();
writer2.start();
writer3.start();
writer1.stop();
writer2.stop();
writer3.stop();
ml::core_t::TTime end(ml::core::CTimeUtils::now());
LOG_INFO(<< "Finished fast mutex lock throughput test at "
<< ml::core::CTimeUtils::toTimeString(end));
LOG_INFO(<< "Fast mutex lock throughput test with test size "
<< TEST_SIZE << " took " << (end - start) << " seconds");
LOG_INFO(<< "Fast mutex lock protected variable incremented to " << testVariable);
BOOST_REQUIRE_EQUAL(TEST_SIZE * (1 + 5 + 9), testVariable);
}
{
std::uint32_t testVariable(0);
ml::core::CMutex mutex;
ml::core_t::TTime start(ml::core::CTimeUtils::now());
LOG_INFO(<< "Starting mutex lock throughput test at "
<< ml::core::CTimeUtils::toTimeString(start));
CMutexProtectedAdder writer1(mutex, 0, TEST_SIZE, 1, testVariable);
CMutexProtectedAdder writer2(mutex, 0, TEST_SIZE, 5, testVariable);
CMutexProtectedAdder writer3(mutex, 0, TEST_SIZE, 9, testVariable);
writer1.start();
writer2.start();
writer3.start();
writer1.stop();
writer2.stop();
writer3.stop();
ml::core_t::TTime end(ml::core::CTimeUtils::now());
LOG_INFO(<< "Finished mutex lock throughput test at "
<< ml::core::CTimeUtils::toTimeString(end));
LOG_INFO(<< "Mutex lock throughput test with test size " << TEST_SIZE
<< " took " << (end - start) << " seconds");
LOG_INFO(<< "Mutex lock protected variable incremented to " << testVariable);
BOOST_REQUIRE_EQUAL(TEST_SIZE * (1 + 5 + 9), testVariable);
}
{
std::uint32_t testVariable(0);
ml::core::CReadWriteLock readWriteLock;
ml::core_t::TTime start(ml::core::CTimeUtils::now());
LOG_INFO(<< "Starting read-write lock throughput test at "
<< ml::core::CTimeUtils::toTimeString(start));
CWriteLockProtectedAdder writer1(readWriteLock, 0, TEST_SIZE, 1, testVariable);
CWriteLockProtectedAdder writer2(readWriteLock, 0, TEST_SIZE, 5, testVariable);
CWriteLockProtectedAdder writer3(readWriteLock, 0, TEST_SIZE, 9, testVariable);
writer1.start();
writer2.start();
writer3.start();
writer1.stop();
writer2.stop();
writer3.stop();
ml::core_t::TTime end(ml::core::CTimeUtils::now());
LOG_INFO(<< "Finished read-write lock throughput test at "
<< ml::core::CTimeUtils::toTimeString(end));
LOG_INFO(<< "Read-write lock throughput test with test size "
<< TEST_SIZE << " took " << (end - start) << " seconds");
LOG_INFO(<< "Write lock protected variable incremented to " << testVariable);
BOOST_REQUIRE_EQUAL(TEST_SIZE * (1 + 5 + 9), testVariable);
}
}
BOOST_AUTO_TEST_SUITE_END()