I came across a post titled C++ Locking Wrapper shared by Meeting C++ on Twitter and it reminded me of Synchronized Data Structures and boost::synchronized_value so I decided to implement my own version as a learning exercise and possible topic of a future video on my YT channel.
The idea is simple: synchronize across multiple threads every method call to an instance of an object; do it in the most transparent and un-obstructing way possible. Here’s a simple example illustrating the idea:
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 |
#include <thread> #include <vector> #include <cstdlib> #include "synchronized.hpp" int main() { using namespace std; srand(time(NULL)); synchronized<vector<int>> sv; //auto sv = new vector<int>; thread t1([&] () { while(true) sv->push_back(rand()); }); thread t2([&] () { while(true) sv->clear(); }); t1.join(); t2.join(); } |
The trick comes down to two things really: 1) Wrap a value of type T and its lock (in my case std::mutex) in a containing class and 2) Override operator->() to return a RAII temporary responsible for guarding the value.
I will add that this is perhaps a heavy handed approach to guarding data since I do not know how to make it transparent while allowing reader/writer locks, or synchronization of only some select methods. Perhaps some type system hackery with const and volatile methods could help here…
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 |
#include <mutex> #include <utility> #include <type_traits> namespace detail { template<typename U> class locker { public: locker(U& v, std::recursive_mutex& l) noexcept : m_value(v), m_lock(l) { m_lock.lock(); } locker(const locker&) = delete; locker& operator = (const locker&) = delete; ~locker() noexcept { m_lock.unlock(); } U* operator -> () noexcept { return &m_value; } private: U& m_value; std::recursive_mutex& m_lock; }; } template<typename T> class synchronized { public: synchronized() = default; synchronized(const T& v) : m_value(v) {} synchronized(T&& v) : m_value(std::move(v)) {} template<typename... A, std::enable_if_t< std::is_constructible_v<T, A...>>* = nullptr> synchronized(A&&... a) : m_value(std::forward<A>(a)...) {} template<typename V, std::enable_if_t< std::is_constructible_v<T, std::initializer_list<V>>>* = nullptr> synchronized(std::initializer_list<V> l) : m_value(l) {} synchronized(const synchronized&) = delete; synchronized& operator = (const synchronized&) = delete; detail::locker<T> operator -> () noexcept { return detail::locker(m_value, m_lock); } private: T m_value; std::recursive_mutex m_lock; }; |
One Reply to “How to synchronize data access”