The usual pattern of using std::shared_timed_mutex is to let the 'reader' thread acquire it in shared mode and the 'writer' thread acquires it in exclusive mode. In this manner, the reads and writes cannot happen at the same time and thus the program is free from data-race/undefined behavior.
I wanted to understand if at all there's any problem if I change the mode among the threads i.e. the reader thread reads the shared variable after acquiring the lock in exclusive mode and the writer thread writes in the shared variable after taking the mutex in shared mode.
#include <iostream>
#include <thread>
#include <random>
#include <chrono>
#include <shared_mutex>
using namespace std::chrono_literals;
std::shared_timed_mutex lck;
int shared_array[5];
void writerFunc(int index);
void readerFunc();
//main thread
int main() {
std::thread writer_threads[5];
for(int i=0; i<5; ++i) {
writer_threads[i] = std::thread(writerFunc,i);
}
while(true) {
std::this_thread::sleep_for(5s);
readerFunc();
}
for(int i=0; i<5; ++i) {
writer_threads[i].join();
}
}
//function executed in writer threads.
//Each writer thread will work on it's own index in the global shared array.
void writerFunc(int index) {
std::random_device rd;
std::mt19937 mt(rd());
std::uniform_real_distribution<double> dist(1.0, 42.0);
while(true) {
{
std::shared_lock<std::shared_timed_mutex> sl(lck);
//Writing random number in shared variable.
shared_array[index] += dist(mt);
}
std::this_thread::sleep_for(100ms);
}
}
//function executed in reader thread(main).
void readerFunc() {
std::lock_guard<std::shared_timed_mutex> sl(lck);
for(int i=0; i<5 ; ++i) {
std::cout<<"\nshared_array["<<i<<"]--> "<<shared_array[i];
}
std::cout<<"\n\n";
}
Since the reader and writer thread cannot concurrently access the variable at the same time, therefore, there's no data race in the above program. Thread-sanitiser also does not report any problem with the above program.
I'm mainly having a little doubt regarding the values read by the reader thread.
Is it guaranteed by the C++ standard, irrespective of the underlying CPU architecture, that
a) the above program doesn't have any UB?
b) the reader thread can only see the latest value written by the writer thread?
******* Additional details ********
Please note that the above was a short sample program where I've tried to replicate a specific part of the design of my main project. Over there the scale is a lot bigger. e.g. the size of the array(not exactly an array but very similar) over there is ~2 million. Also the data structure is not a simple int but a custom serializable structure.
so think of something like this:
custom_serializable_struct shared_variable[2000000];
In my main program, there will be 'N' writer threads and a single reader thread. Most of the time, writer threads will be working. Since N is a lot smaller than 2 million therefore I'm using separate synchronisation(1 std::atomic_flag for each of the 2 million indexes. This is used after acquiring the shared_timed_mutex) among the writer threads(I had omitted this aspect from the design of the sample code as I felt it was not relevant to what I was asking).
Like I've said above, most of the time, writer threads will be working. Only occasionally, the reader thread will work.
Mainly, the program has following requirements:
- I've to minimize the wait time of writer threads spent on the mutex while the reader thread is working.
- I've to ensure that the reader thread, whenever it works, always gets the latest value written by the writer threads.
So basically this is what is happening in my main program:
N writer threads:
while (true) {
// 1. Acquire the shared_timed_mutex in shared mode.
// 2. Acquire the std::atomic_flag of the index, i, on which the thread has to work. This is required, as I mentioned, to prevent data race among writer threads.
// 3. Do changes in the custom_serializable_struct shared_variable[i]
}
1 reader thread:
while(true) {
// 1. long sleep time.
// 2. Acquire the shared_timed_mutex in exclusive mode.
// 3. read the entire 2 million values. Please note that this read is not done 1 by 1 like in a for loop. It's more like memcpy of the entire memory.
}