I've written an implementation of the reader-writers problem using shared_timed_mutex of C++14. In my opinion the following code should cause the Writer to starve since too many reader threads are working on the database (in this example a simple array) all the time: The writer has no chance to acquire the lock.
mutex cout_mtx; // controls access to standard output
shared_timed_mutex db_mtx; // controls access to data_base
int data_base[] = { 0, 0, 0, 0, 0, 0 };
const static int NR_THREADS_READ = 10;
const static int NR_THREADS_WRITE = 1;
const static int SLEEP_MIN = 10;
const static int SLEEP_MAX = 20;
void read_database(int thread_nr) {
shared_lock<shared_timed_mutex> lck(db_mtx, defer_lock); // create a lock based on db_mtx but don't try to acquire the mutex yet
while (true) {
// generate new random numbers
std::random_device r;
std::default_random_engine e(r());
std::uniform_int_distribution<int> uniform_dist(SLEEP_MIN, SLEEP_MAX);
std::uniform_int_distribution<int> uniform_dist2(0, 5);
int sleep_duration = uniform_dist(e); // time to sleep between read requests
int read_duration = uniform_dist(e); // duration of reading from data_base
int cell_number = uniform_dist2(e); // what data cell will be read from
int cell_value = 0;
// wait some time before requesting another access to the database
this_thread::sleep_for(std::chrono::milliseconds(sleep_duration));
if (!lck.try_lock()) {
lck.lock(); // try to get the lock in blocked state
}
// read data
cell_value = data_base[cell_number];
lck.unlock();
}
}
void write_database(int thread_nr) {
unique_lock<shared_timed_mutex> lck(db_mtx, defer_lock); // create a lock based on db_mtx but don't try to acquire the mutex yet
while (true) {
// generate new random numbers
std::random_device r;
std::default_random_engine e(r());
std::uniform_int_distribution<int> uniform_dist(SLEEP_MIN, SLEEP_MAX);
std::uniform_int_distribution<int> uniform_dist2(0, 5);
int sleep_duration = uniform_dist(e); // time to sleep between write requests
int read_duration = uniform_dist(e); // duration of writing to data_base
int cell_number = uniform_dist2(e); // what data cell will be written to
// wait some time before requesting another access to the database
this_thread::sleep_for(std::chrono::milliseconds(sleep_duration));
// try to get exclusive access
cout_mtx.lock();
cout << "Writer <" << thread_nr << "> requesting write access." << endl;
cout_mtx.unlock();
if (!lck.try_lock()) {
lck.lock(); // try to get the lock in blocked state
}
// write data
data_base[cell_number] += 1;
lck.unlock();
}
}
I added some output to standard output when a thread is reading, writing, trying to acquire the lock either in blocked mode or via the try_lock()
method but I deleted the output for the sake of clarity. I start the threads further down in the main method. When I run the program the writer always gets the chance to write to the array (causing all of the reader threads to block, which is ok) but as I said above the writer should not be able to get access at all since there are too many reader threads reading from the array. Even when I don't let the reader threads to sleep at all (argument 0) the writer threads somehow finds a way to get the mutex. How do I get the writer to starve then?
A quality implementation of std::shared_timed_mutex
will not starve readers nor writers. However as the number of readers / number of writers grows, the lesser the probability that the writers get the lock. With your current setting (1 writer to 10 readers) I'm guessing that the writer gets the lock about 9% of the time. As you increase that ratio, the writer will get the lock less, but will never be 100% starved out.
If you only let the writer acquire under a try_lock
, then your chances of starving it 100% will greatly increase.
The existence of the algorithms which allow std::shared_timed_mutex
to be implemented without starving readers nor writers is the reason that std::shared_timed_mutex
does not have an API that allows you to dictate reader-priority or writer-priority.
The Algorithm
Imagine that there are two gates within the mutex: gate1
and gate2
.
To get past gate1
it (almost) doesn't matter whether you are a reader or a writer. If there is another writer inside of gate1
, you can't get in. Readers have to follow an additional rule that in practice never comes into play: If there are already the maximum number of readers past gate1
, you can't get in.
Once past gate1
, a reader owns the shared lock.
Once past gate1
, a writer does not own the unique lock. He must further wait outside gate2
until there are no more readers holding the shared lock. Once past gate2
, the writer owns the unique lock.
This algorithm is "fair" in that it makes little difference if you are a reader or writer to get past gate1
. If there are a bunch of readers and writers outside of gate1
, the next thread to get in is decided by the OS, not by this algorithm. So you can think of it as a roll of the dice. If you have the same number of readers as writers competing for gate1
, it is a 50/50 chance whether a reader or a writer is the next one to get past gate1
.