Here’s yet another idea I had for a C++ type. It’s a useful tool for simplifying the use of a mutex with a shared resource.
Before I show you what I made I’ll demonstrate its purpose with a small demo program. I wrote this to intentionally produce errors due to a multithreaded race condition.
static pair<int,int>& getPair()
{
static pair<int,int> p{0,0};
return p;
}
int main()
{
vector<thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.emplace_back([]()
{
auto& p = getPair();
p.first = p.second;
// sleep here to coax a context switch in the kernel,
// making an error more likely
this_thread::sleep_for(chrono::milliseconds(1));
p.second += 1;
if (p.second - p.first != 1)
cout << "Hey, that's not right!" << endl;
});
}
for (auto& t : threads)
t.join();
return 0;
}Every single time I ran this on my computer it resulted in numerous errors, due to the threads interleaving their increments on the shared pair object. Here’s an example of output, where even the console print instructions fell on top of each other.
Hey, that's not right!
Hey, that's not right!Hey, that's not right!
Hey, that's not right!Hey, that's not right!
Hey, that's not right!
Hey, that's not right!
Hey, that's not right!
Hey, that's not right!This is what you would expect to see, it’s a classic example of bad multithreaded code. Here’s how I fixed it using my new value_lock type.
static value_lock< pair<int,int>, mutex > getPair()
{
static pair<int,int> p{0,0};
static mutex mtx;
return value_lock{p, mtx};
}
int main()
{
vector<thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.emplace_back([]()
{
auto p = getPair();
p->first = p->second;
this_thread::sleep_for(chrono::milliseconds(1));
p->second += 1;
if (p.second - p.first != 1)
cout << "Hey, that's not right!" << endl;
});
}
for (auto& t : threads) t.join();
return 0;
}What gets returned by getPair() holds both a mutex and a reference to the underlying object. On construction the mutex is locked, and on destruction unlocked. In each thread, exclusive access to the underlying reference is guaranteed within the scope of the p variable. This solves the race condition bug in a way that is much more concise and readable than to have used the mutex separately.
My new type has another neat trick, which I borrowed the maybe_ptr type I wrote about previously.
static value_lock< pair<int,int>, mutex > getPair()
{
static pair<int,int> p{0,0};
static mutex mtx;
return value_lock{p, mtx};
}
int main()
{
vector<thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.emplace_back([]()
{
for (auto&& p: getPair())
{
p.first = p.second;
this_thread::sleep_for(chrono::milliseconds(1));
p.second += 1;
if (p.second - p.first != 1)
cout << "Hey, that's not right!" << endl;
}
});
}
for (auto& t : threads) t.join();
return 0;
}This solves the problem just as before. Using (abusing) the ‘zero or one’ iterator pattern to create a separate scope in which the underlying resource can be accessed exclusively. This use-case ends up being very powerful if you were to use a type of mutex that is non-blocking. It allows you to say concisely, “Do this if the shared resource can be locked, otherwise do nothing”.
Here’s the actual implementation for value_lock.
template<typename T> class value_lock_iterator
{
public:
using iterator_category = std::random_access_iterator_tag;
using value_type = std::remove_const_t<T>;
using difference_type = std::ptrdiff_t;
using pointer = T*;
using reference = T&;
constexpr value_lock_iterator(T* p = nullptr) : ptr(p) {}
constexpr reference operator*() const { return *ptr; }
constexpr pointer operator->() const { return ptr; }
constexpr value_lock_iterator& operator++() { ptr = nullptr; return *this; }
constexpr value_lock_iterator operator++(int) { auto tmp = *this; ++(*this); return tmp; }
constexpr bool operator==(value_lock_iterator const& other) const { return ptr == other.ptr; }
constexpr bool operator!=(value_lock_iterator const& other) const { return ptr != other.ptr; }
private:
T* ptr;
};
template <typename V, typename M = std::mutex> class value_lock
{
public:
using iterator = value_lock_iterator<V>;
using const_iterator = value_lock_iterator<V const>;
explicit value_lock(V& value, M& mtx)
: m_value(value), m_lock(mtx)
{}
value_lock(value_lock const&) = delete;
value_lock& operator=(value_lock const&) = delete;
V& value() { return m_value; }
V const& value() const { return m_value; }
V& operator*() { return m_value; }
V* operator->() { return &m_value; }
V const& operator*() const { return m_value; }
V const* operator->() const { return &m_value; }
iterator begin() { return iterator(&m_value); }
iterator end() { return iterator(nullptr); }
const_iterator begin() const { return const_iterator(&m_value); }
const_iterator end() const { return const_iterator(nullptr); }
const_iterator cbegin() const { return const_iterator(&m_value); }
const_iterator cend() const { return const_iterator(nullptr); }
private:
V& m_value;
std::lock_guard<M> m_lock; // RAII lock management
};I’ve been using this in my most recent project and it has been very useful for me. I’m making my own programming language called Algoma, and I have been using this in the desktop script editor. Algoma scripts need to be executed off the UI thread to avoid potential freezing, and I’ve been using value_lock to mediate resource sharing between UI and runner threads.
Did you find this useful? Do you have any interesting C++ tricks of your own? If so, let me know!
Send a message to mail@lzon.ca, or DM me on one of my social accounts on the homepage.