blob: 16b77deb64346806f43650ffbdb26bf321d108ae [file] [log] [blame]
/*----------------------------------------------------------------------------*/
/* Copyright (c) FIRST 2008. All Rights Reserved. */
/* Open Source Software - may be modified and shared by FRC teams. The code */
/* must be accompanied by the FIRST BSD license file in $(WIND_BASE)/WPILib. */
/*----------------------------------------------------------------------------*/
#include <taskLib.h>
#include <intLib.h>
#include <assert.h>
#include <tickLib.h>
#include "RWLock.h"
// A wrapper for assert that allows it to be easily turned off just in this
// file. That configuration is recommended for normal use because it means less
// code that gets executed with the scheduler locked.
#if 1
#define rwlock_assert(expression) assert(expression)
// A macro to easily assert that some expression (possibly with side effects)
// is 0.
#define rwlock_assert_success(expression) do { \
int ret = (expression); \
assert(ret == 0); \
} while (false)
#else
#define rwlock_assert(expression) ((void)0)
#define rwlock_assert_success(expression) ((void)(expression))
#endif
/**
* Class that locks the scheduler and then unlocks it in the destructor.
*/
class TaskSchedulerLocker {
public:
TaskSchedulerLocker() {
rwlock_assert_success(taskLock());
}
~TaskSchedulerLocker() {
rwlock_assert_success(taskUnlock());
}
private:
DISALLOW_COPY_AND_ASSIGN(TaskSchedulerLocker);
};
RWLock::Locker::Locker(RWLock *lock, bool write)
: lock_(lock), num_(lock_->Lock(write)) {
}
RWLock::Locker::Locker(const Locker &other)
: lock_(other.lock_), num_(lock_->AddLock()) {
}
RWLock::Locker::~Locker() {
lock_->Unlock(num_);
}
// RWLock is implemented by just locking the scheduler while doing anything
// because that is the only way under vxworks to do much of anything atomically.
RWLock::RWLock()
: number_of_write_locks_(0),
number_of_writers_pending_(0),
number_of_readers_(0),
reader_tasks_(),
read_ready_(semBCreate(SEM_Q_PRIORITY, SEM_EMPTY)),
write_ready_(semBCreate(SEM_Q_PRIORITY, SEM_EMPTY)) {
rwlock_assert(read_ready_ != NULL);
rwlock_assert(write_ready_ != NULL);
}
RWLock::~RWLock() {
// Make sure that nobody else currently has a lock or will ever be able to.
Lock(true);
rwlock_assert_success(semDelete(read_ready_));
rwlock_assert_success(semDelete(write_ready_));
}
int RWLock::Lock(bool write) {
assert(!intContext());
int current_task = taskIdSelf();
// It's safe to do this check up here (outside of locking the scheduler)
// because we only care whether the current task is in there or not and that
// can't be changed because it's the task doing the checking.
bool current_task_holds_already = TaskOwns(current_task);
TaskSchedulerLocker scheduler_locker;
taskSafe();
// We can't be reading and writing at the same time.
rwlock_assert(!((number_of_write_locks_ > 0) && (number_of_readers_ > 0)));
if (write) {
assert(!current_task_holds_already);
// If somebody else already has it locked.
// Don't have to worry about another task getting scheduled after
// write_ready_ gets given because nobody else (except another writer, which
// would just block on it) will do anything while there are pending
// writer(s).
if ((number_of_readers_ > 0) || (number_of_write_locks_ > 0)) {
++number_of_writers_pending_;
// Wait for it to be our turn.
rwlock_assert_success(semTake(write_ready_, WAIT_FOREVER));
--number_of_writers_pending_;
} else {
rwlock_assert(number_of_writers_pending_ == 0);
}
rwlock_assert((number_of_write_locks_ == 0) && (number_of_readers_ == 0));
number_of_write_locks_ = 1;
return 0;
} else { // read
// While there are one or more writers active or waiting.
// Has to be a loop in case a writer gets scheduled between the time
// read_ready_ gets flushed and we run.
while ((number_of_write_locks_ > 0) || (number_of_writers_pending_ > 0)) {
// Wait for the writer(s) to finish.
rwlock_assert_success(semTake(read_ready_, WAIT_FOREVER));
}
int num = number_of_readers_;
number_of_readers_ = num + 1;
assert(num < kMaxReaders);
rwlock_assert(reader_tasks_[num] == 0);
reader_tasks_[num] = current_task;
rwlock_assert((number_of_write_locks_ == 0) && (number_of_readers_ > 0));
return num;
}
}
void RWLock::Unlock(int num) {
assert(!intContext());
TaskSchedulerLocker scheduler_locker;
taskUnsafe();
// We have to be reading or writing right now, but not both.
rwlock_assert((number_of_write_locks_ > 0) != (number_of_readers_ > 0));
if (number_of_write_locks_ > 0) { // we're currently writing
rwlock_assert(num == 0);
--number_of_write_locks_;
rwlock_assert((number_of_write_locks_ >= 0) &&
(number_of_writers_pending_ >= 0));
// If we were the last one.
if (number_of_write_locks_ == 0) {
// If there are no other tasks waiting to write (because otherwise they
// need to get priority over any readers).
if (number_of_writers_pending_ == 0) {
// Wake up any waiting readers.
rwlock_assert_success(semFlush(read_ready_));
} else {
// Wake up a waiting writer.
// Not a problem if somebody else already did this before the waiting
// writer got a chance to take it because it'll do nothing and return
// success.
rwlock_assert_success(semGive(write_ready_));
}
}
} else { // we're curently reading
rwlock_assert(reader_tasks_[num] == taskIdSelf());
reader_tasks_[num] = 0;
--number_of_readers_;
rwlock_assert(number_of_readers_ >= 0 &&
(number_of_writers_pending_ >= 0));
// If we were the last one.
if (number_of_readers_ == 0) {
// If there are any writers waiting for a chance to go.
if (number_of_writers_pending_ > 0) {
// Wake a waiting writer.
// Not a problem if somebody else already did this before the waiting
// writer got a chance to take it because it'll still return success.
rwlock_assert_success(semGive(write_ready_));
}
}
}
}
int RWLock::AddLock() {
assert(!intContext());
// TODO: Replace this with just atomically incrementing the right number once
// we start using a GCC new enough to have the nice atomic builtins.
// That will be safe because whether we're currently reading or writing can't
// change in the middle of this.
TaskSchedulerLocker scheduler_locker;
// We have to be reading or writing right now, but not both.
rwlock_assert((number_of_write_locks_ > 0) != (number_of_readers_ > 0));
if (number_of_write_locks_ > 0) { // we're currently writing
++number_of_write_locks_;
return 0;
} else { // we're currently reading
return number_of_readers_++;
}
}
bool RWLock::TaskOwns(int task_id) {
for (size_t i = 0;
i < sizeof(reader_tasks_) / sizeof(reader_tasks_[0]);
++i) {
if (reader_tasks_[i] == task_id) return true;
}
return false;
}
#include <stdint.h>
#include "Task.h"
namespace {
namespace testing {
// It's kind of hard to test for race conditions because (by definition) they
// only happen with really specific (and uncommon) timing. However, what tests
// can cover is "normal" functioning (locking/unlocking by multiple tasks).
// How long to wait until "everything" will be done doing whatever it's going
// to.
const int kSettleTicks = 10;
void SetUp() {
}
void TearDown() {
}
struct LockerConfig {
RWLock *const lock;
const bool write;
const int delay_ticks;
bool started;
bool locked;
bool done;
bool unlocked;
LockerConfig(RWLock *lock, bool write, int delay_ticks = kSettleTicks)
: lock(lock), write(write), delay_ticks(delay_ticks),
started(false), locked(false), done(false), unlocked(false) {}
};
void LockerTask(LockerConfig *config) {
config->started = true;
{
RWLock::Locker locker(config->lock, config->write);
config->locked = true;
taskDelay(config->delay_ticks);
config->done = true;
}
config->unlocked = true;
}
// A basic test to make sure that 2 readers can get to it at the same time.
// Mostly just to make sure that the test setup etc works.
bool TwoReaders() {
Task one("R1", reinterpret_cast<FUNCPTR>(LockerTask));
Task two("R2", reinterpret_cast<FUNCPTR>(LockerTask));
RWLock lock;
LockerConfig one_config(&lock, false), two_config(&lock, false);
one.Start(reinterpret_cast<uintptr_t>(&one_config));
two.Start(reinterpret_cast<uintptr_t>(&two_config));
while (!one_config.locked) taskDelay(1);
assert(!one_config.done);
while (!two_config.locked) taskDelay(1);
if (one_config.done) {
printf("It took too long for the second one to lock.\n");
return false;
}
return true;
}
// Makes sure that everything works correctly even if a task is deleted while
// a lock is held.
bool DeleteWhileLocked() {
Task reader("reader", reinterpret_cast<FUNCPTR>(LockerTask));
Task writer("writer", reinterpret_cast<FUNCPTR>(LockerTask));
static const unsigned int kDelayTicks = 15;
RWLock lock;
LockerConfig reader_config(&lock, false, kDelayTicks);
LockerConfig writer_config(&lock, true, kDelayTicks);
ULONG start = tickGet();
reader.Start(reinterpret_cast<uintptr_t>(&reader_config));
while (!reader_config.locked) taskDelay(1);
writer.Start(reinterpret_cast<uintptr_t>(&writer_config));
reader.Stop();
if (tickGet() - start < kDelayTicks) {
printf("Reader stopped too quickly.\n");
return false;
}
while (!writer_config.done) taskDelay(1);
if (tickGet() - start < kDelayTicks * 2) {
printf("Writer finished too quickly.\n");
return false;
}
return true;
}
#define RUN_TEST(name) do { \
SetUp(); \
bool test_succeeded = name(); \
TearDown(); \
if (!test_succeeded) successful = false; \
} while (false)
extern "C" int rwlock_test() {
bool successful = true;
RUN_TEST(TwoReaders);
RUN_TEST(DeleteWhileLocked);
return successful ? 0 : -1;
}
#undef RUN_TEST
} // namespace testing
} // namespace