Home

Programs running multiple threads can be rather painful to maintain. Let's try to keep things simple, by assuming an application with just one additional thread running. Logically it should be a fairly simple process of identifying all data items that are accessed in the extra thread, then seeing where these data items are accessed back in the main thread. For all those data items accessed in both threads, we need to implement a code lock around every point where the data items are accessed. So then it's a question of trying to organize code to best accomplish this, usually by implementing small methods which handle the data access locking, and which the main processing code calls. Like I say, sounds simple, but as code never stands still, it can be an ongoing headache.

I, for one, don't have the perfect answer, but I've been pondering on it and came up with what is shown below. Reinventing the wheel I'm sure, but never mind, it's the principle one wishes to convey.

All it is is this: Putting a single data item, eg. a string or integer, inside a small class wrapper which also encapsulates the code locking when the data item is updated or read. Instances of the class will be given a reference to an external mutex to be used as the lock. Thus, during development, whenever a single item of data is now found to be accessed in multiple threads, the idea is that this particular data item should be converted by wrapping it inside this class. Dreadfully inefficient? I don't know.

Here is such a class, in template form, and called MutexValue:

template <typename T>
class MutexValue
{
private:
    T m_val{};	// Braces to init the variable.
    std::mutex& pMutex = gbl_mutex;

public:
    MutexValue() {}

    // Copy constructor
    MutexValue (const T& val)
    {
	m_val = val;
    }

    // Assignment operator
    void operator= (const T& val)
    {
	{ // LOCK GUARD
 	    std::lock_guard<std::mutex> lock (pMutex);
	    m_val = val;
	    //Sleep (500);	// Artificial pause to illustrate lock
	}
    }

    // Functor
    operator T()
    {
	{ // LOCK GUARD
	    std::lock_guard<std::mutex> lock (pMutex);
	    return m_val;
	}
    }
};

Note that pMutex (yes, I know it's not a pointer, but a reference) is initialized from an external mutex; you customize this bit for your application. Aside from basic constructor and copy constructor, there is an assignment operator for updating the data value, and a functor for reading: These last two are the critical methods where the code locking is applied.

To demonstrate this in a simple Windows console application (created with Visual Studio 2019), here is my main routine:

int main (int argc, char* argv[])
{
    std::cout << "Press RETURN to finish.n";
    std::string sInput;
    Sleep (1000);

    std::thread t1, t2;
    t1 = std::thread (&Foo);
    t2 = std::thread (&Bar);

    getline (std::cin, sInput);

    gbl_fooQuit = true;
    gbl_barQuit = true;

    if (t1.joinable())
	t1.join();
    if (t2.joinable())
	t2.join();

    return 0;
}

For this demo we've got a couple of globals declared:

// Globals
std::mutex gbl_mutex;
MutexValue<std::string> gbl_text = "Hello";
MutexValue<bool> gbl_fooQuit;
MutexValue<bool> gbl_barQuit;

We've got a mutex and three data items wrapped in MutexValue. gbl_text is the main data item that the two worker threads are going to be reading/writing, but we also have the boolean flags wrapped, because they are also accessed in two threads.

So first the program pauses for a second to inform the user how to stop the perishing thing, and then launches two threads where the action really occurs. It waits at the getline for user input, and upon receipt of a RETURN, proceeds to set two flags to tell the threads to finish. The two threads cease and desist, we all greet each other back in the main thread, the program ends and we all go down the pub.

The t1 thread executes function Foo(), which simply loops around endlessly reading gbl_text:

void Foo()
{
    while (true)
    {
	bool bQ = gbl_fooQuit;
	if (bQ)
	    break;

	std::string s = gbl_text;
	std::cout << "Foo:" << s << std::endl;
	Sleep (250);
    }
}

The t2 thread executes function Bar(), which has the job of endlessly looping to change the value of gbl_text:

void Bar()
{
    int i = 0;

    while (true)
    {
	bool bQ = gbl_barQuit;
	if (bQ)
	    break;

	if (i == 0)
	    gbl_text = "Andy";
	else if (i == 1)
	    gbl_text = "Bill";
	else if (i == 2)
	    gbl_text = "Chas";
	else if (i == 3)
	    gbl_text = "Dave";
	else if (i == 4)
	    gbl_text = "Eric";

	Sleep (1000);

	i++;
	if (i > 4)
	    i = 0;
    }
}

The MutexValue assignment operator and functor make it very easy to manipulate the core data item wrapped inside it - it looks as though you are manipulating the underlying data object directly. If you run this in debug your Windows IDE and step through the code you will see how it invokes the code locks inside MutexValue.

The MutexValue class, of course, should implement better code for resolving the mutex it expects to use. So then it's just a matter of organizing your mutex objects appropriately for various discrete sections of your large complex application.

Okay, so it's pretty simple and may not, as it is, easily facilitate complex data items such as complete class objects. Feel free to add more sophistication.

Once again, here's the demo code in full:

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

template <typename T>
class MutexValue
{
private:
    T m_val{};	// Braces to init the variable.
    std::mutex& pMutex = gbl_mutex;

public:
    MutexValue() {}

    // Copy constructor
    MutexValue (const T& val)
    {
	m_val = val;
    }

    // Assignment operator
    void operator= (const T& val)
    {
	{ // LOCK GUARD
	    std::lock_guard<std::mutex> lock (pMutex);
	    m_val = val;
	    //Sleep (500);	// Artificial pause to illustrate lock
	}
    }

    // Functor
    operator T()
    {
	{ // LOCK GUARD
	    std::lock_guard<std::mutex> lock (pMutex);
	    return m_val;
	}
    }
};

// Globals
std::mutex gbl_mutex;
MutexValue<bool> gbl_fooQuit;
MutexValue<bool> gbl_barQuit;
MutexValue<std::string> gbl_text = "Hello";

void Foo()
{
    while (true)
    {
	bool bQ = gbl_fooQuit;
	if (bQ)
	    break;

	std::string s = gbl_text;
	std::cout << "Foo:" << s << std::endl;
	Sleep (250);
    }
}

void Bar()
{
    int i = 0;

    while (true)
    {
	bool bQ = gbl_barQuit;
	if (bQ)
	    break;

	if (i == 0)
	    gbl_text = "Andy";
	else if (i == 1)
	    gbl_text = "Bill";
	else if (i == 2)
	    gbl_text = "Chas";
	else if (i == 3)
	    gbl_text = "Dave";
	else if (i == 4)
	    gbl_text = "Eric";

	Sleep (1000);

	i++;
	if (i > 4)
	    i = 0;
    }
}

int main (int argc, char* argv[])
{
    std::cout << "Press RETURN to finish.n";
    std::string sInput;
    Sleep (1000);

    std::thread t1, t2;
    t1 = std::thread (&Foo);
    t2 = std::thread (&Bar);

    getline (std::cin, sInput);

    gbl_fooQuit = true;
    gbl_barQuit = true;

    if (t1.joinable())
	t1.join();
    if (t2.joinable())
	t2.join();

    return 0;
}
Comments
Post a commentAuthorize
Comment...
Cancel
Please enter/paste your User Key Token to authorize comment activity.
(Need a User Key Token?)
Cancel
Request a User Key Token
so you can post comments in response to articles.
Email Address
Choose a Username
Thank you. Please now check your email.
theKendallizer2024-05-09 13:30:48ReplyDelete
First! ;-)
8
© 2019 - 2024 Kendallization