Skip to content

Commit 472ca33

Browse files
committed
qrwlock as quick rwlock without starvation prevention but better perfomance for read-heavy workloads
1 parent 89edfda commit 472ca33

2 files changed

Lines changed: 304 additions & 2 deletions

File tree

thread/test/test.cpp

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,192 @@ TEST(future, test2) {
18911891
EXPECT_EQ(v, PROMISE_VALUE);
18921892
}
18931893

1894+
TEST(qrwlock, BasicLockUnlock) {
1895+
qrwlock mtx;
1896+
1897+
// Test exclusive lock
1898+
EXPECT_EQ(0, mtx.lock(WLOCK));
1899+
mtx.unlock();
1900+
1901+
// Test shared lock
1902+
EXPECT_EQ(0, mtx.lock(RLOCK));
1903+
mtx.unlock();
1904+
}
1905+
1906+
TEST(qrwlock, TryLock) {
1907+
qrwlock mtx;
1908+
1909+
// Try lock should succeed when unlocked
1910+
EXPECT_TRUE(mtx.try_lock(WLOCK) == 0);
1911+
// Try lock should fail when write-locked
1912+
EXPECT_FALSE(mtx.try_lock(WLOCK) == 0);
1913+
EXPECT_FALSE(mtx.try_lock(RLOCK) == 0);
1914+
mtx.unlock();
1915+
1916+
// Try shared lock should succeed when unlocked
1917+
EXPECT_TRUE(mtx.try_lock(RLOCK) == 0);
1918+
// Additional shared locks should succeed
1919+
EXPECT_TRUE(mtx.try_lock(RLOCK) == 0);
1920+
// Write lock should fail when read-locked
1921+
EXPECT_FALSE(mtx.try_lock(WLOCK) == 0);
1922+
mtx.unlock();
1923+
mtx.unlock();
1924+
}
1925+
1926+
TEST(qrwlock, MultipleReaders) {
1927+
qrwlock mtx;
1928+
constexpr int NUM_READERS = 100;
1929+
1930+
// Acquire many shared locks
1931+
for (int i = 0; i < NUM_READERS; i++) {
1932+
EXPECT_TRUE(mtx.try_lock(RLOCK) == 0);
1933+
}
1934+
1935+
// Write lock should fail
1936+
EXPECT_FALSE(mtx.try_lock(WLOCK) == 0);
1937+
1938+
// Release all shared locks
1939+
for (int i = 0; i < NUM_READERS; i++) {
1940+
mtx.unlock();
1941+
}
1942+
1943+
// Now write lock should succeed
1944+
EXPECT_TRUE(mtx.try_lock(WLOCK) == 0);
1945+
mtx.unlock();
1946+
}
1947+
1948+
TEST(qrwlock, ConcurrentReaders) {
1949+
qrwlock mtx;
1950+
std::atomic<int> active_readers{0};
1951+
std::atomic<int> max_concurrent{0};
1952+
std::atomic<uint64_t> total_ops{0};
1953+
constexpr int NUM_THREADS = 32;
1954+
constexpr int OPS_PER_THREAD = 1000;
1955+
1956+
std::vector<photon::join_handle*> handles;
1957+
for (int i = 0; i < NUM_THREADS; i++) {
1958+
handles.emplace_back(photon::thread_enable_join(
1959+
photon::thread_create11([&]() {
1960+
for (int j = 0; j < OPS_PER_THREAD; j++) {
1961+
mtx.lock(RLOCK);
1962+
int cur = active_readers.fetch_add(1, std::memory_order_relaxed) + 1;
1963+
// Track max concurrent readers
1964+
int prev_max = max_concurrent.load(std::memory_order_relaxed);
1965+
while (cur > prev_max &&
1966+
!max_concurrent.compare_exchange_weak(prev_max, cur)) {}
1967+
photon::thread_yield();
1968+
active_readers.fetch_sub(1, std::memory_order_relaxed);
1969+
mtx.unlock();
1970+
total_ops.fetch_add(1, std::memory_order_relaxed);
1971+
}
1972+
})));
1973+
}
1974+
1975+
for (auto& h : handles) {
1976+
photon::thread_join(h);
1977+
}
1978+
1979+
EXPECT_EQ(0, active_readers.load());
1980+
EXPECT_GT(max_concurrent.load(), 1); // Should have concurrent readers
1981+
EXPECT_EQ(NUM_THREADS * OPS_PER_THREAD, total_ops.load());
1982+
LOG_INFO("Max concurrent readers: `", max_concurrent.load());
1983+
}
1984+
1985+
TEST(qrwlock, ReaderWriterExclusion) {
1986+
qrwlock mtx;
1987+
std::atomic<bool> writing{false};
1988+
std::atomic<int> active_readers{0};
1989+
std::atomic<uint64_t> read_ops{0};
1990+
std::atomic<uint64_t> write_ops{0};
1991+
constexpr int NUM_READERS = 16;
1992+
constexpr int NUM_WRITERS = 4;
1993+
constexpr int OPS_PER_THREAD = 500;
1994+
1995+
std::vector<photon::join_handle*> handles;
1996+
1997+
// Reader threads
1998+
for (int i = 0; i < NUM_READERS; i++) {
1999+
handles.emplace_back(photon::thread_enable_join(
2000+
photon::thread_create11([&]() {
2001+
for (int j = 0; j < OPS_PER_THREAD; j++) {
2002+
mtx.lock(RLOCK);
2003+
active_readers.fetch_add(1, std::memory_order_relaxed);
2004+
EXPECT_FALSE(writing.load(std::memory_order_acquire));
2005+
photon::thread_yield();
2006+
EXPECT_FALSE(writing.load(std::memory_order_acquire));
2007+
active_readers.fetch_sub(1, std::memory_order_relaxed);
2008+
mtx.unlock();
2009+
read_ops.fetch_add(1, std::memory_order_relaxed);
2010+
}
2011+
})));
2012+
}
2013+
2014+
// Writer threads
2015+
for (int i = 0; i < NUM_WRITERS; i++) {
2016+
handles.emplace_back(photon::thread_enable_join(
2017+
photon::thread_create11([&]() {
2018+
for (int j = 0; j < OPS_PER_THREAD; j++) {
2019+
mtx.lock(WLOCK);
2020+
EXPECT_EQ(0, active_readers.load(std::memory_order_acquire));
2021+
writing.store(true, std::memory_order_release);
2022+
photon::thread_yield();
2023+
EXPECT_EQ(0, active_readers.load(std::memory_order_acquire));
2024+
writing.store(false, std::memory_order_release);
2025+
mtx.unlock();
2026+
write_ops.fetch_add(1, std::memory_order_relaxed);
2027+
}
2028+
})));
2029+
}
2030+
2031+
for (auto& h : handles) {
2032+
photon::thread_join(h);
2033+
}
2034+
2035+
EXPECT_EQ(NUM_READERS * OPS_PER_THREAD, read_ops.load());
2036+
EXPECT_EQ(NUM_WRITERS * OPS_PER_THREAD, write_ops.load());
2037+
LOG_INFO("Read ops: `, Write ops: `", read_ops.load(), write_ops.load());
2038+
}
2039+
2040+
TEST(qrwlock, LockTimeout) {
2041+
qrwlock mtx;
2042+
2043+
// Acquire write lock in main coroutine
2044+
EXPECT_EQ(0, mtx.lock(WLOCK));
2045+
2046+
// Try to acquire read lock with timeout in another coroutine
2047+
auto jh = photon::thread_enable_join(
2048+
photon::thread_create11([&]() {
2049+
EXPECT_EQ(-1, mtx.lock(RLOCK, 100 * 1000));
2050+
EXPECT_EQ(ETIMEDOUT, errno);
2051+
}));
2052+
2053+
// Must yield to let the waiter run and timeout
2054+
photon::thread_usleep(200 * 1000); // Sleep longer than timeout
2055+
2056+
photon::thread_join(jh);
2057+
mtx.unlock();
2058+
}
2059+
2060+
TEST(qrwlock, WriteLockTimeout) {
2061+
qrwlock mtx;
2062+
2063+
// Acquire read lock in main coroutine
2064+
EXPECT_EQ(0, mtx.lock(RLOCK));
2065+
2066+
// Try to acquire write lock with timeout in another coroutine
2067+
auto jh = photon::thread_enable_join(
2068+
photon::thread_create11([&]() {
2069+
EXPECT_EQ(-1, mtx.lock(WLOCK, 100 * 1000));
2070+
EXPECT_EQ(ETIMEDOUT, errno);
2071+
}));
2072+
2073+
// Must yield to let the waiter run and timeout
2074+
photon::thread_usleep(200 * 1000); // Sleep longer than timeout
2075+
2076+
photon::thread_join(jh);
2077+
mtx.unlock();
2078+
}
2079+
18942080
int main(int argc, char** arg)
18952081
{
18962082
if (!photon::is_using_default_engine()) return 0;

thread/thread.h

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,12 +549,16 @@ namespace photon
549549
class rwlock
550550
{
551551
public:
552+
int try_lock(int mode);
552553
int lock(int mode, Timeout timeout = {});
553554
int unlock();
554555
protected:
555-
int64_t state = 0;
556+
std::atomic<int64_t> state{0};
556557
condition_variable cvar;
557-
mutex mtx;
558+
condition_variable cvar_uniq;
559+
spinlock mtx;
560+
constexpr static int64_t MAX_SHARED_LOCK_COUNT = 1 << 16;
561+
constexpr static int64_t WRITE_LOCKED = -1;
558562
};
559563

560564
class scoped_rwlock
@@ -589,6 +593,118 @@ namespace photon
589593
bool m_locked;
590594
};
591595

596+
// High-performance rwlock without starvation prevention.
597+
// Optimized for read-heavy workloads using lock-free atomic operations
598+
// for the read lock fast path.
599+
class qrwlock {
600+
protected:
601+
constexpr static int64_t MAX_SHARED_LOCK_COUNT = 1 << 16;
602+
constexpr static int64_t WRITE_LOCKED = -1;
603+
604+
// State encoding:
605+
// state > 0 : number of active readers
606+
// state == 0 : unlocked
607+
// state == -1: write-locked
608+
609+
std::atomic<int64_t> lock_state{0};
610+
photon::condition_variable cv_shared;
611+
photon::condition_variable cv_unique;
612+
photon::spinlock spin;
613+
614+
// Must be called with spinlock held.
615+
void try_wake() {
616+
if (!cv_unique.notify_one()) {
617+
cv_shared.notify_all();
618+
}
619+
}
620+
621+
// Common blocking lock pattern: fast-path try, then slow-path wait
622+
// loop. try_fn must be safe to call both with and without spinlock
623+
// held.
624+
template <typename TryFunc>
625+
int do_lock(TryFunc&& try_fn, photon::condition_variable& cv,
626+
Timeout timeout) {
627+
if (try_fn()) return 0;
628+
SCOPED_LOCK(spin);
629+
while (true) {
630+
if (try_fn()) return 0;
631+
int ret = cv.wait(spin, timeout);
632+
if (ret < 0) {
633+
return -1;
634+
}
635+
}
636+
}
637+
638+
void __unlock_unique() {
639+
SCOPED_LOCK(spin);
640+
assert(lock_state.load(std::memory_order_relaxed) == WRITE_LOCKED);
641+
lock_state.store(0, std::memory_order_release);
642+
try_wake();
643+
}
644+
645+
void __unlock_shared() {
646+
auto prev = lock_state.fetch_sub(1, std::memory_order_acq_rel);
647+
assert(prev > 0); // prev<=0 indicates misuse
648+
if (prev == 1) {
649+
SCOPED_LOCK(spin);
650+
try_wake();
651+
}
652+
}
653+
654+
bool __trylock() {
655+
int64_t expected = 0;
656+
return lock_state.compare_exchange_strong(
657+
expected, WRITE_LOCKED, std::memory_order_acq_rel,
658+
std::memory_order_relaxed);
659+
}
660+
661+
bool __trylock_shared() {
662+
auto state = lock_state.load(std::memory_order_acquire);
663+
while (state >= 0 && state < MAX_SHARED_LOCK_COUNT) {
664+
if (lock_state.compare_exchange_weak(state, state + 1,
665+
std::memory_order_acq_rel,
666+
std::memory_order_acquire))
667+
return true;
668+
}
669+
return false;
670+
}
671+
672+
public:
673+
qrwlock() = default;
674+
qrwlock(const qrwlock&) = delete;
675+
qrwlock& operator=(const qrwlock&) = delete;
676+
677+
int try_lock(int mode) {
678+
if (mode == WLOCK)
679+
return __trylock() ? 0 : -1;
680+
else
681+
return __trylock_shared() ? 0 : -1;
682+
}
683+
684+
int lock(int mode, Timeout timeout = {}) {
685+
if (mode == WLOCK)
686+
return do_lock([this] { return __trylock(); }, cv_unique,
687+
timeout);
688+
else
689+
return do_lock([this] { return __trylock_shared(); }, cv_shared,
690+
timeout);
691+
}
692+
693+
int unlock() {
694+
auto cur_state = lock_state.load(std::memory_order_acquire);
695+
if (cur_state == 0) {
696+
errno = ENOLCK;
697+
return -1;
698+
}
699+
if (cur_state == WRITE_LOCKED) {
700+
__unlock_unique();
701+
} else {
702+
__unlock_shared();
703+
}
704+
return 0;
705+
}
706+
};
707+
592708
// create `n` threads to run `start(arg)`, then get joined
593709
void threads_create_join(uint64_t n, thread_entry start, void* arg,
594710
uint64_t stack_size = DEFAULT_STACK_SIZE);

0 commit comments

Comments
 (0)