Here's a test assignment I was given that I apparently failed:
1.Use two threads to increment an integer. Thread A increments when even and Thread B increments when odd (for the integer problem we can have it specify up to a number provided on the command line)
1a. What are some of the difficulties in adding more threads? Please show the difficulties with code.
1b. Extra credit – Design an improved solution to the above that can scale with many threads
The feedback after the first try was "didn't address atomic modification and false sharing". I attempted to address them, but there was no feedback for the second attempt. I want to use this test to learn, so I figured I'd ask the topmost experts - you.
The following is the header for the first try:
#include <iostream>
#include <mutex>
#include <atomic>
class CIntToInc
{
private:
int m_nVal; //std::atomic<int> m_nVal;
int m_nMaxVal;
public:
CIntToInc(int p_nVal, int p_nMaxVal) : m_nVal(p_nVal), m_nMaxVal(p_nMaxVal) { }
const int GetVal() const { return m_nVal; }
const int GetMaxVal() const { return m_nMaxVal; }
void operator ++() { ++m_nVal; }
};
struct COper
{
enum class eOper { None = 0, Mutex = 1, NoMutex = 2 };
eOper m_Oper;
public:
friend std::istream& operator>> (std::istream &in, COper &Oper);
bool operator == (const eOper &p_eOper) { return(m_Oper == p_eOper); }
};
The following is the source for the first try. It includes my thoughts as to why the solution would work. I compiled the code in MSVS2012.
// Notes:
// 1a.
// Since an integer cannot be an odd number and an even number at the same time, thread separation happens naturally when each thread checks the value.
// This way no additional synchronization is necessary and both threads can run at will, provided that it's all they are doing.
// It's probably not even necessary to declare the target value atomic because it changes (and thus lets the other thread increment itself) only at the last moment.
// I would still opt for making it atomic.
// Adding more threads to this setup immediately creates a problem with threads of equal condition (even or odd) stepping on each other.
// 1b.
// By using a mutex threads can cleanly separate. Many threads with the same condition can run concurrently.
// Note: there is no guarantee that each individual thread from a pool of equally conditioned threads will get to increment the number.
// For this method reading has to be inside the mutext lock to prevent a situation where a thread may see the value as incrementable, yet when it gets to it, the value has already
// been changed by another thread and no longer qualifies.
// cout message output is separated in this approach.
//
// The speed of the "raw" approach is 10 times faster than that of the mutex approach on an equal number of threads (two) with the mutex time increasing further as you add threads.
// Use 10000000 for the max to feel the difference, watch the CPU graph
//
// If the operation is complex and time consuming, the approach needs to be different still. The "increment" functionality can be wrapped up in a pimpl class, a copy can be made
// and "incremented". When ready, the thread will check for whether the value has changed while the operation was being performed on the copy and, if not, a fast swap under the mutex
// could be attempted. This approach is resource-intensive, but it mininuzes lock time.
//
// The approach above will work if the operation does not involve resources that cannot be easily copied (like a file to the end of which we are writing)
// When such resources are present, the algorithm probably has to implement a thread safe queue.
// END
#include "test.h"
#include <thread>
int main_test();
int main(int argc, char* argv[])
{
main_test();
return(0);
}
void IncrementInt2(CIntToInc &p_rIi, bool p_bIfEven, const char *p_ThreadName, std::mutex *p_pMu)
// the version that uses a mutex
// enable cout output to see thread messages
{
int nVal(0);
while(true) {
p_pMu->lock();
bool DoWork = (nVal = p_rIi.GetVal() < p_rIi.GetMaxVal());
if(DoWork) {
//std::cout << "Thread " << p_ThreadName << ": nVal=" << nVal << std::endl;
if((!(nVal % 2) && p_bIfEven) || (nVal % 2 && !p_bIfEven)) {
//std::cout << "incrementing" << std::endl;
++p_rIi; } }
p_pMu->unlock();
if(!DoWork) break;
//if(p_bIfEven) // uncomment to force threads to execute differently
// std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void IncrementInt3(CIntToInc &p_rIi, bool p_bIfEven, const char *p_ThreadName)
// the version that does not use a mutex
// enable cout output to see thread messages. Message text output is not synchronized
{
int nVal(0);
while((nVal = p_rIi.GetVal()) < p_rIi.GetMaxVal()) {
//std::cout << "Thread " << p_ThreadName << ": nVal=" << nVal << std::endl;
if((!(nVal % 2) && p_bIfEven) || (nVal % 2 && !p_bIfEven)) {
//std::cout << "Thread " << p_ThreadName << " incrementing" << std::endl;
++p_rIi; }
}
}
std::istream& operator>> (std::istream &in, COper &Oper)
// to read operation types from cin
{
int nVal;
std::cin >> nVal;
switch(nVal) {
case 1: Oper.m_Oper = COper::eOper::Mutex; break;
case 2: Oper.m_Oper = COper::eOper::NoMutex; break;
default: Oper.m_Oper = COper::eOper::None; }
return in;
}
int main_test()
{
int MaxValue, FinalValue;
COper Oper;
std::cout << "Please enter the number to increment to: ";
std::cin >> MaxValue;
std::cout << "Please enter the method (1 - mutex, 2 - no mutex): ";
std::cin >> Oper;
auto StartTime(std::chrono::high_resolution_clock::now());
if(Oper == COper::eOper::Mutex) {
std::mutex Mu;
CIntToInc ii(0, MaxValue);
std::thread teven(IncrementInt2, std::ref(ii), true, "Even", &Mu);
std::thread todd(IncrementInt2, std::ref(ii), false, "Odd", &Mu);
// add more threads at will, should be safe
//std::thread teven2(IncrementInt2, std::ref(ii), true, "Even2", &Mu);
//std::thread teven3(IncrementInt2, std::ref(ii), true, "Even3", &Mu);
teven.join();
todd.join();
//teven2.join();
//teven3.join();
FinalValue = ii.GetVal();
}
else if(Oper == COper::eOper::NoMutex) {
CIntToInc ii(0, MaxValue);
std::thread teven(IncrementInt3, std::ref(ii), true, "Even");
std::thread todd(IncrementInt3, std::ref(ii), false, "Odd");
teven.join();
todd.join();
FinalValue = ii.GetVal(); }
std::chrono::duration<double>elapsed_seconds = (std::chrono::high_resolution_clock::now() - StartTime);
std::cout << "main_mutex completed with nVal=" << FinalValue << " in " << elapsed_seconds.count() << " seconds" << std::endl;
return(0);
}
For the second attempt I made the following changes to the header:
made m_nVal std::atomic
used atomic methods to increment and retrieve m_nVal
separated m_nVal from the read-only m_nMaxVal by a filler
The source file was unchanged. The new header is below.
#include <iostream>
#include <mutex>
#include <atomic>
class CIntToInc
{
private:
int m_nMaxVal;
char m_Filler[64 - sizeof(int)]; // false sharing prevention, assuming a 64 byte cache line
std::atomic<int> m_nVal;
public:
CIntToInc(int p_nVal, int p_nMaxVal) : m_nVal(p_nVal), m_nMaxVal(p_nMaxVal) { }
const int GetVal() const {
//return m_nVal;
return m_nVal.load(); // std::memory_order_relaxed);
}
const int GetMaxVal() const { return m_nMaxVal; }
void operator ++() {
//++m_nVal;
m_nVal.fetch_add(1); //, std::memory_order_relaxed); // relaxed is enough since we check this very variable
}
};
struct COper
{
enum class eOper { None = 0, Mutex = 1, NoMutex = 2 };
eOper m_Oper;
public:
friend std::istream& operator>> (std::istream &in, COper &Oper);
bool operator == (const eOper &p_eOper) { return(m_Oper == p_eOper); }
};
I don't know if the approach is fundamentally wrong, or if there is one or more smaller errors.