Thread Contention in C++
A huge sticking point in multi-threaded programming is concurrent access of the same variable. The simple solution to this problem is to create a mutex, and lock the mutex in any part of code that touches your “danger” variable. A slightly more sophisticated access pattern is the “read-write” lock. Essentially, any number of threads can read from a variable at the same time, but only one thread can write to a variable at any time. When the “write-lock” is acquired, “read-locks” cannot be acquired either.
The Problem with the Solution
The perfect programmer will always accompany any accesses of this “danger” variable with the proper lock acquisition. The imperfect programmer (read: me) might miss one or two areas of code that access the variable. So, I decided to dummy-proof the read-write lock access pattern.
The Implementation
I wanted to write a container that can hold any data type, and exposes a “Get” (read) and an “Access” (write). The object returned by “Get” will acquire a read lock throughout the lifetime of its existence, and likewise, the object returned by “Access” will acquire a write lock.
Here’s LockedContainer.h
#pragma once
#include <shared_mutex>
#include <mutex>
template <typename T>
class LockedContainer
{
public:
LockedContainer() = default;
LockedContainer(const T& data) : m_data(data) {};
class ReadContainer
{
public:
ReadContainer(const T& data, std::shared_mutex& mutex) : m_lock(mutex), m_data(data) {};
const T& operator->() { return m_data; };
const T& operator*() { return m_data; };
private:
const T& m_data;
std::shared_lock<std::shared_mutex> m_lock;
};
class WriteContainer
{
public:
WriteContainer(T& data, std::shared_mutex& mutex) : m_lock(mutex), m_data(data) {};
T& operator->() { return m_data; };
T& operator*() { return m_data; };
void operator=(const T& val) { m_data = val; };
private:
T& m_data;
std::unique_lock<std::shared_mutex> m_lock;
};
ReadContainer Get()
{
return ReadContainer(m_data, m_mutex);
}
WriteContainer Access()
{
return WriteContainer(m_data, m_mutex);
}
void operator=(const T& val)
{
*(Access()) = val;
}
private:
T m_data;
std::shared_mutex m_mutex;
};
Awesome! Now, if you wanted to declare a lock-protected int, as a member variable of a class, you declare it like this:
LockedContainer<int> m_sharedInt;
And if you want to read-lock a section of your code, you call this:
LockedContainer<int>::ReadContainer container = m_sharedInt.Get();
Similarly, a write-lock would look like this:
LockedContainer<int>::WriteContainer container = m_sharedInt.Access();
Or, if you were simply setting the variable, you can do a quick write lock like this:
m_sharedInt = 0;
Testing
I wrote a quick main function that tested this access pattern. It spawns 200 threads, every 10 of which are write functions. The rest are read functions. At the end, I print the contents of the variable, and the time (in microseconds) it took to acquire the lock.
#include "LockedContainer.h"
#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#define NUM_ITERATIONS 200
std::string results[NUM_ITERATIONS];
LockedContainer<int> sharedInt;
void ReadFunc(int i)
{
auto start = std::chrono::high_resolution_clock::now();
LockedContainer<int>::ReadContainer container = sharedInt.Get();
auto end = std::chrono::high_resolution_clock::now();
results[i] = "Read=" + std::to_string(*container) + " (" + std::to_string(std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()) + " us)";
}
void WriteFunc(int i)
{
auto start = std::chrono::high_resolution_clock::now();
LockedContainer<int>::WriteContainer container = sharedInt.Access();
auto end = std::chrono::high_resolution_clock::now();
(*container)++;
results[i] = "Write=" + std::to_string(*container) + " (" + std::to_string(std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()) + " us)";
}
int main()
{
sharedInt = 0;
std::thread threads[NUM_ITERATIONS];
for (int i = 0; i < NUM_ITERATIONS; i++)
{
if (i % 10 == 0)
{
threads[i] = std::thread(WriteFunc, i);
}
else
{
threads[i] = std::thread(ReadFunc, i);
}
}
for (int i = 0; i < NUM_ITERATIONS; i++)
{
threads[i].join();
std::cout << results[i].c_str() << std::endl;
}
return 0;
}
All in all, looks pretty good, and is easy to use. I’m happy with how this little escapade turned out.