I was under the impression a multiple reader / single writer pattern implemented with c++17's std::shared_mutex
could potentially never relinquish unique locks if too many shared locks were acquired.
After digging on cppreference, I am unsure that is the case. Specifically :
All lock and unlock operations on a single mutex occur in a single total order
For example, given the following operations on a shared_mutex
, I believed the unique_lock
may never acquire. Assuming an infinite amount of shared_locks
, and that these locks acquire before the first shared_locks
release.
shared_lock
shared_lock
shared_lock
unique_lock
shared_lock
[...]
shared_lock
Giving the following characteristics.
{ shared_lock, shared_lock, shared_lock, shared_lock, ..., shared_lock } // never releases
unique_lock
However, if I understand cppreference correctly, once the unique_lock
tries to acquire, consecutive shared_locks
would block until the unique_lock
is released. Giving the following threading characteristics.
{ shared_lock, shared_lock, shared_lock} // simultaneous
unique_lock
{ shared_lock, ..., shared_lock} // waits, then simultaneous
So my question is, does a std::shared_mutex
keep ordering between shared and unique locks? Preventing the case where unique_locks
are never acquired due to an overwhelming amount of shared_locks
being acquired.
edit :
Here is a code example to help understand the problem, and for posterity's sake. On MSVC 2019, shared_mutex
is safe and ordering happens as desired. The unique_lock
does get processed before the "infinite" amount of shared_locks
.
The question now becomes, is this platform dependent?
#include <chrono>
#include <cstdio>
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
std::shared_mutex smtx;
int main(int, char**) {
std::vector<std::thread> threads;
auto read_task = [&]() {
std::shared_lock l{ smtx };
printf("read\n");
std::this_thread::sleep_for(1s);
};
auto write_task = [&]() {
std::unique_lock l{ smtx };
printf("write\n");
std::this_thread::sleep_for(1s);
};
// Create a few reader tasks.
threads.emplace_back(read_task);
threads.emplace_back(read_task);
threads.emplace_back(read_task);
// Try to lock a unique_lock before read tasks are done.
std::this_thread::sleep_for(1ms);
threads.emplace_back(write_task);
// Then, enque a gazillion read tasks.
// Will the unique_lock be locked? [drum roll]
// Would be while(true), 120 should be enough for demo
for (size_t i = 0; i < 120; ++i) {
std::this_thread::sleep_for(1ms);
threads.emplace_back(read_task);
}
for (auto& t : threads) {
t.join();
}
}
Outputs :
read
read
read
write
read
...
read