blob: 513d01405a84711f308f0dafe52c220074d541bd [file] [edit]
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include <wtf/ReadWriteLock.h>
#include <array>
#include <wtf/Atomics.h>
#include <wtf/Lock.h>
#include <wtf/MonotonicTime.h>
#include <wtf/Threading.h>
#include <wtf/Vector.h>
namespace TestWebKitAPI {
namespace {
constexpr Seconds testTimeout = 30_s;
// Every test here can hang rather than fail if the lock is broken, so each one runs its
// threads with a deadline and reports how far each thread got. A hang shows up as a
// failed expectation on the per-thread progress counters instead of a stuck test binary.
class DeadlineGuard {
public:
bool expired() const { return MonotonicTime::now() > m_deadline; }
private:
MonotonicTime m_deadline { MonotonicTime::now() + testTimeout };
};
template<typename Body>
Vector<Ref<Thread>> spawn(unsigned count, Body body)
{
Vector<Ref<Thread>> threads;
for (unsigned i = 0; i < count; ++i) {
threads.append(Thread::create("ReadWriteLock test"_s, [i, body] {
body(i);
}));
}
return threads;
}
void join(Vector<Ref<Thread>>& threads)
{
for (auto& thread : threads)
thread->waitForCompletion();
}
} // anonymous namespace
// Three or more writers contending must all make progress. A writer must never spin with the lock
// free, which is what a phase handed off without being claimed would cause.
TEST(WTF_ReadWriteLock, ManyWritersMakeProgress)
{
constexpr unsigned writerCount = 8;
constexpr unsigned acquisitionsPerWriter = 1000;
ReadWriteLock lock;
DeadlineGuard deadline;
unsigned counter = 0;
std::array<Atomic<unsigned>, writerCount> progress { };
auto threads = spawn(writerCount, [&](unsigned index) {
for (unsigned i = 0; i < acquisitionsPerWriter && !deadline.expired(); ++i) {
Locker locker { lock.write() };
++counter;
progress[index].store<MemoryOrder::Relaxed>(i + 1);
}
});
join(threads);
for (unsigned i = 0; i < writerCount; ++i)
EXPECT_EQ(acquisitionsPerWriter, progress[i].load<MemoryOrder::Relaxed>());
EXPECT_EQ(writerCount * acquisitionsPerWriter, counter);
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// A stream of writers must not starve readers: with several writers always queued, the reader wake
// path still has to run on every phase end.
TEST(WTF_ReadWriteLock, WritersDoNotStarveReaders)
{
constexpr unsigned writerCount = 4;
constexpr unsigned readerCount = 4;
constexpr unsigned readsPerReader = 100;
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<bool> readersDone { false };
unsigned counter = 0;
std::array<Atomic<unsigned>, readerCount> readsCompleted { };
auto writers = spawn(writerCount, [&](unsigned) {
while (!readersDone.load<MemoryOrder::Relaxed>() && !deadline.expired()) {
Locker locker { lock.write() };
++counter;
}
});
auto readers = spawn(readerCount, [&](unsigned index) {
for (unsigned i = 0; i < readsPerReader && !deadline.expired(); ++i) {
Locker locker { lock.read() };
readsCompleted[index].store<MemoryOrder::Relaxed>(i + 1);
}
});
join(readers);
readersDone.store<MemoryOrder::Relaxed>(true);
join(writers);
for (unsigned i = 0; i < readerCount; ++i)
EXPECT_EQ(readsPerReader, readsCompleted[i].load<MemoryOrder::Relaxed>());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// The mirror case: a stream of readers must not starve a writer. A writer registers
// itself before inspecting the lock, which holds new readers out.
TEST(WTF_ReadWriteLock, ReadersDoNotStarveWriter)
{
constexpr unsigned readerCount = 4;
constexpr unsigned writesToComplete = 100;
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<bool> writerDone { false };
Atomic<unsigned> writesCompleted { 0 };
auto readers = spawn(readerCount, [&](unsigned) {
while (!writerDone.load<MemoryOrder::Relaxed>() && !deadline.expired())
Locker locker { lock.read() };
});
auto writer = spawn(1, [&](unsigned) {
for (unsigned i = 0; i < writesToComplete && !deadline.expired(); ++i) {
Locker locker { lock.write() };
writesCompleted.store<MemoryOrder::Relaxed>(i + 1);
}
});
join(writer);
writerDone.store<MemoryOrder::Relaxed>(true);
join(readers);
EXPECT_EQ(writesToComplete, writesCompleted.load<MemoryOrder::Relaxed>());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// Several writers plus readers, so that WTF::Lock's fair handoff - which writer exclusion delegates
// to - runs with other writers still parked behind it. A woken writer still has to claim a phase of
// its own; if that bookkeeping leaks, a later reader or writer hangs.
TEST(WTF_ReadWriteLock, WriterHandoffWithWritersAndReadersQueued)
{
constexpr unsigned writerCount = 6;
constexpr unsigned readerCount = 6;
constexpr unsigned iterations = 500;
ReadWriteLock lock;
DeadlineGuard deadline;
unsigned counter = 0;
std::array<Atomic<unsigned>, writerCount + readerCount> progress { };
auto writers = spawn(writerCount, [&](unsigned index) {
for (unsigned i = 0; i < iterations && !deadline.expired(); ++i) {
Locker locker { lock.write() };
++counter;
progress[index].store<MemoryOrder::Relaxed>(i + 1);
}
});
auto readers = spawn(readerCount, [&](unsigned index) {
for (unsigned i = 0; i < iterations && !deadline.expired(); ++i) {
Locker locker { lock.read() };
progress[writerCount + index].store<MemoryOrder::Relaxed>(i + 1);
}
});
join(writers);
join(readers);
for (unsigned i = 0; i < writerCount + readerCount; ++i)
EXPECT_EQ(iterations, progress[i].load<MemoryOrder::Relaxed>());
EXPECT_EQ(writerCount * iterations, counter);
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// Once a burst of writers has finished and every thread has gone away, a lone reader
// must still be able to acquire. This catches state left behind on behalf of a writer
// that no longer exists, which is invisible while any writer is still running.
TEST(WTF_ReadWriteLock, LoneReaderAfterWriterBurst)
{
constexpr unsigned writerCount = 4;
constexpr unsigned iterations = 2000;
ReadWriteLock lock;
DeadlineGuard deadline;
unsigned counter = 0;
auto writers = spawn(writerCount, [&](unsigned) {
for (unsigned i = 0; i < iterations && !deadline.expired(); ++i) {
Locker locker { lock.write() };
++counter;
}
});
join(writers);
EXPECT_TRUE(lock.isQuiescentForTesting());
Atomic<bool> acquired { false };
auto reader = spawn(1, [&](unsigned) {
Locker locker { lock.read() };
acquired.store<MemoryOrder::Relaxed>(true);
});
join(reader);
EXPECT_TRUE(acquired.load<MemoryOrder::Relaxed>());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// Mutual exclusion: a writer must never overlap a reader or another writer.
TEST(WTF_ReadWriteLock, MutualExclusion)
{
constexpr unsigned writerCount = 4;
constexpr unsigned readerCount = 4;
constexpr unsigned iterations = 500;
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<int> readersInside { 0 };
Atomic<int> writersInside { 0 };
Atomic<unsigned> violations { 0 };
auto writers = spawn(writerCount, [&](unsigned) {
for (unsigned i = 0; i < iterations && !deadline.expired(); ++i) {
Locker locker { lock.write() };
writersInside.exchangeAdd(1);
if (writersInside.load() != 1 || readersInside.load())
violations.exchangeAdd(1);
Thread::yield();
if (writersInside.load() != 1 || readersInside.load())
violations.exchangeAdd(1);
writersInside.exchangeAdd(-1);
}
});
auto readers = spawn(readerCount, [&](unsigned) {
for (unsigned i = 0; i < iterations && !deadline.expired(); ++i) {
Locker locker { lock.read() };
readersInside.exchangeAdd(1);
if (writersInside.load())
violations.exchangeAdd(1);
Thread::yield();
if (writersInside.load())
violations.exchangeAdd(1);
readersInside.exchangeAdd(-1);
}
});
join(writers);
join(readers);
EXPECT_EQ(0u, violations.load());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// Readers really are concurrent: with no writers present, several readers must be able
// to hold the lock at the same time.
TEST(WTF_ReadWriteLock, ReadersRunConcurrently)
{
constexpr unsigned readerCount = 4;
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<unsigned> arrived { 0 };
Atomic<unsigned> inside { 0 };
Atomic<unsigned> maxObserved { 0 };
auto readers = spawn(readerCount, [&](unsigned) {
Locker locker { lock.read() };
unsigned current = inside.exchangeAdd(1) + 1;
for (;;) {
unsigned previous = maxObserved.load();
if (current <= previous || maxObserved.compareExchangeWeak(previous, current))
break;
}
// Wait on a count that only ever grows. Waiting on `inside` would hang until the
// deadline, because the last reader to arrive decrements it again on its way out
// before the others have observed it.
arrived.exchangeAdd(1);
while (arrived.load() < readerCount && !deadline.expired())
Thread::yield();
inside.exchangeAdd(-1);
});
join(readers);
EXPECT_EQ(readerCount, maxObserved.load());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// A writer must announce itself before it blocks, not only once it holds the lock. That announcement
// is what holds new readers out, and without it a stream of readers could starve writers.
TEST(WTF_ReadWriteLock, WriterAnnouncesItselfBeforeBlocking)
{
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<bool> writerHasLock { false };
lock.readLock();
auto writer = spawn(1, [&](unsigned) {
Locker locker { lock.write() };
writerHasLock.store<MemoryOrder::Relaxed>(true);
});
while (!lock.hasWriterPresentForTesting() && !deadline.expired())
Thread::yield();
// Announced, but it cannot have the lock: we still hold a read lock it has to drain.
EXPECT_TRUE(lock.hasWriterPresentForTesting());
EXPECT_FALSE(writerHasLock.load<MemoryOrder::Relaxed>());
lock.readUnlock();
join(writer);
EXPECT_TRUE(writerHasLock.load<MemoryOrder::Relaxed>());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// A writer parked behind a reader must be woken when that reader leaves.
TEST(WTF_ReadWriteLock, WriterParkedBehindReaderIsWoken)
{
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<bool> readerLeaving { false };
Atomic<bool> writerSawReaderLeave { false };
lock.readLock();
auto writer = spawn(1, [&](unsigned) {
Locker locker { lock.write() };
writerSawReaderLeave.store<MemoryOrder::Relaxed>(readerLeaving.load<MemoryOrder::Relaxed>());
});
// The writer sets its parked bit only once it has stopped spinning, and cannot then decline to
// park while we hold the read lock.
while (!lock.hasParkedBitsForTesting() && !deadline.expired())
Thread::yield();
EXPECT_TRUE(lock.hasParkedBitsForTesting());
readerLeaving.store<MemoryOrder::Relaxed>(true);
lock.readUnlock();
join(writer);
EXPECT_TRUE(writerSawReaderLeave.load<MemoryOrder::Relaxed>());
EXPECT_FALSE(lock.hasParkedBitsForTesting());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// Readers parked behind a writer must all be woken when it leaves.
TEST(WTF_ReadWriteLock, ReadersParkedBehindWriterAreWoken)
{
constexpr unsigned readerCount = 4;
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<unsigned> readersArrived { 0 };
Atomic<bool> writerLeaving { false };
std::array<Atomic<bool>, readerCount> readerSawWriterLeave { };
Vector<Ref<Thread>> readers;
{
Locker locker { lock.write() };
readers = spawn(readerCount, [&](unsigned index) {
readersArrived.exchangeAdd(1);
Locker locker { lock.read() };
readerSawWriterLeave[index].store<MemoryOrder::Relaxed>(writerLeaving.load<MemoryOrder::Relaxed>());
});
while ((readersArrived.load() < readerCount || !lock.hasParkedBitsForTesting()) && !deadline.expired())
Thread::yield();
EXPECT_EQ(readerCount, readersArrived.load());
EXPECT_TRUE(lock.hasParkedBitsForTesting());
writerLeaving.store<MemoryOrder::Relaxed>(true);
}
join(readers);
for (unsigned i = 0; i < readerCount; ++i)
EXPECT_TRUE(readerSawWriterLeave[i].load<MemoryOrder::Relaxed>());
// A reader that stops spinning just as the writer leaves can set its parked bit too late to be
// woken and then decline to park, leaving the bit for the next writer to clear.
{
Locker locker { lock.write() };
}
EXPECT_FALSE(lock.hasParkedBitsForTesting());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// A reader that is slow to be scheduled after being woken must not lose its turn. With two
// readers parked and a writer looping, the writer's release wakes both, but the first reader
// can complete its whole critical section and the writer can re-acquire before the second
// reader ever runs. If waking only granted permission rather than the lock itself, the second
// reader would find that permission withdrawn, re-park, and repeat forever.
TEST(WTF_ReadWriteLock, WokenReaderKeepsItsTurnWhenDescheduled)
{
constexpr unsigned readerCount = 2;
constexpr unsigned readsPerReader = 200;
ReadWriteLock lock;
DeadlineGuard deadline;
Atomic<bool> readersDone { false };
Atomic<unsigned> writesCompleted { 0 };
std::array<Atomic<unsigned>, readerCount> readsCompleted { };
// One writer, looping as tightly as it can, so that it is always either holding the lock
// or registered as waiting for it.
auto writer = spawn(1, [&](unsigned) {
while (!readersDone.load<MemoryOrder::Relaxed>() && !deadline.expired()) {
Locker locker { lock.write() };
writesCompleted.exchangeAdd(1);
}
});
auto readers = spawn(readerCount, [&](unsigned index) {
// Don't start until the writer is genuinely running, otherwise the readers can finish
// before it ever contends and the test proves nothing.
while (writesCompleted.load<MemoryOrder::Relaxed>() < 100 && !deadline.expired())
Thread::yield();
for (unsigned i = 0; i < readsPerReader && !deadline.expired(); ++i) {
{
Locker locker { lock.read() };
// Hold long enough that the other reader in the cohort is likely still
// descheduled when this one releases, which is the shape of the race.
Thread::yield();
}
readsCompleted[index].store<MemoryOrder::Relaxed>(i + 1);
}
});
join(readers);
readersDone.store<MemoryOrder::Relaxed>(true);
join(writer);
// If this is 0 the writer never contended and the test was vacuous.
EXPECT_GT(writesCompleted.load<MemoryOrder::Relaxed>(), 100u);
for (unsigned i = 0; i < readerCount; ++i)
EXPECT_EQ(readsPerReader, readsCompleted[i].load<MemoryOrder::Relaxed>());
EXPECT_TRUE(lock.isQuiescentForTesting());
}
// Uncontended acquisition must leave no residue, so that the inline fast paths keep
// working rather than silently degrading to the out-of-line ones.
TEST(WTF_ReadWriteLock, UncontendedQuiescence)
{
ReadWriteLock lock;
for (unsigned i = 0; i < 100; ++i) {
{
Locker locker { lock.write() };
}
EXPECT_TRUE(lock.isQuiescentForTesting());
EXPECT_FALSE(lock.hasParkedBitsForTesting());
{
Locker locker { lock.read() };
}
EXPECT_TRUE(lock.isQuiescentForTesting());
EXPECT_FALSE(lock.hasParkedBitsForTesting());
}
}
// A writer may destroy the lock as soon as it has acquired it, so the last reader out must not touch
// the lock once the writer can get in. Each round builds a lock in storage that the writer destroys
// and fills with a poison pattern; any later access from the reader shows up as a changed byte.
TEST(WTF_ReadWriteLock, WriterMayDestroyLockAfterLastReaderReleases)
{
constexpr unsigned pairCount = 8;
constexpr unsigned roundsPerPair = 2000;
DeadlineGuard deadline;
Atomic<unsigned> corruptedRounds { 0 };
Atomic<unsigned> completedRounds { 0 };
auto pairs = spawn(pairCount, [&](unsigned) {
for (unsigned round = 0; round < roundsPerPair && !deadline.expired(); ++round) {
alignas(ReadWriteLock) std::array<uint8_t, sizeof(ReadWriteLock)> storage;
auto* lock = new (storage.data()) ReadWriteLock;
lock->readLock();
auto writer = spawn(1, [&](unsigned) {
lock->writeLock();
lock->writeUnlock();
std::destroy_at(lock);
storage.fill(0xFF);
});
// Wait for the writer to announce itself, and on most rounds for it to park too.
while (!lock->hasWriterPresentForTesting())
Thread::yield();
if (round % 4) {
while (!lock->hasParkedBitsForTesting())
Thread::yield();
}
lock->readUnlock();
join(writer);
for (uint8_t byte : storage) {
if (byte != 0xFF) {
corruptedRounds.exchangeAdd(1);
break;
}
}
completedRounds.exchangeAdd(1);
}
});
join(pairs);
EXPECT_EQ(pairCount * roundsPerPair, completedRounds.load());
EXPECT_EQ(0u, corruptedRounds.load());
}
} // namespace TestWebKitAPI