Yesterday I wrote about How to synchronize data access, you should read it before continuing with this post, it will explain in detail the technique I will expand upon here. I’ll wait…
Alright! Now that you understand how a temporary RAII object can lock a mutex associated with an instance of an object, effectively synchronizing across multiple threads every method call, let’s talk about reader/writer locks.
R/W locks allow for two levels of synchronization: shared and exclusive. Shared lock allows multiple threads to simultaneously access an object under the assumption that said threads will perform only read operations (or to be more exact, operations which do not change the externally observable state of an object). Exclusive lock on the other hand can be held by only one thread at which point said thread is allowed to modify the object’s state and any data associated with it.
In order to implement this functionality I had to create two types of the locker object (the RAII holder of a mutex). Both lockers hold a reference to std::shared_mutex but one locker uses std::shared_lock while the other uses std::unique_lock to acquire ownership of the mutex. This approach is still transparent to the user with the following exception: a non-const instance of shared_synchronized<T> class must use std::as_const in order to acquire shared ownership ( const shared_synchronized<T>, shared_synchronized<const T>, and const shared_synchronized<const T> will always acquire shared lock; note that std::as_const performs a const_cast).
Implementation:
synchronized.hpp | synchronized.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#include <iostream> #include <thread> #include <vector> #include <cstdlib> #include "synchronized.hpp" struct S { void method() { std::cout << "non const\n"; } void method() const { std::cout << "const\n"; } }; int main() { shared_synchronized<S> s1; // takes exclusive lock s1->method(); // takes shared lock std::as_const(s1)->method(); // due to const'ness calling any method on s2, s3, or s4 // will ALWAYS take shared lock; no need for std::as_const const shared_synchronized<S> s2; shared_synchronized<const S> s3; const shared_synchronized<const S> s4; // all 3 calls take shared lock s2->method(); s3->method(); s4->method(); // std::vector shared by 3 threads t1, t2, and t3 // t1 and t2 take exclusive locks, t3 takes shared lock shared_synchronized<std::vector<int>> sv; std::thread t1([&] () { std::srand(std::time(NULL)); while(true) // takes exclusive lock sv->push_back(rand()); }); std::thread t2([&] () { while(true) // takes exclusive lock sv->clear(); }); std::thread t3([&] () { while(true) // takes shared lock [[maybe_unused]] auto _ = std::as_const(sv)->empty(); }); t1.join(); t2.join(); t3.join(); } |