Multithreading - Part 2
by (28 February 2000)



Return to The Archives
Introduction


Last time on "The Young and the Multithreading"... we scratched into the surface of setting up more than one thread using a simple thread wrapper. But one very important part that wasn't discussed was how to protect data that is used by more than one thread.


The Problem With Critical Space Data


Imagine that two threads need to access a linked list of ...stuff.


class cLList
{
protected:
  cLList  *m_pPrev,
          *m_pNext;
  cStuff  m_stuff;
public:
  // contructor, destructor
  // m_stuff access functions
  // cLList management functions
};
 


(if you really needed that explained to you, you probably don't need to be learning about multithreading yet, either (: )

Windows is giving each thread x milliseconds of time to run, based on that thread's priority. So let's say thread A is trying to delete something from the list:


delete unprotectedListItem5;
unprotectedListItem4->SetNext( NULL );
 


and thread B is searching through the list for some information


for( pList = &sharedUnprotectedList; pList != NULL; pList = pList->Next() )
{
  // search for some criteria
}
 


Neither one is a problem UNLESS by some horrible chance Windows decides to switch thread control at a really bad time.


// Thread A is working...
delete unprotectedListItem5;
// Thread B gains priority!
for( pList = &sharedUnprotectedList; pList != NULL; pList = pList->Next() )
{
  // search for some criteria
}
 


Since unprotectedListItem4->SetNext( NULL ); hasn't been called yet, disaster ensues. If you didn't know what you were looking for, the reason for this kind of bug could drive you quite mad. Fortunately for me, I was already pretty far gone, so I didn't have much to lose.

Of course, this is only one of many possible disasters. What should be done about it?


(Enter Mutexes, stage left)


Ever play Railroad Tycoon? The big trick was that you'd waste a lot of money doubling all your track. If two trains wanted to use the same piece of track they'd crash. The solution was to put up switching stations everywhere. The moment a train started to use a piece of track, the switching station lights would change to red, meaning "this track is being used, stop and wait!" The moment that piece of track was empty again the lights would turn green and then it was first come, first serve to see who'd get to go next.

Why the largely unrelated seguay down memory lane? The track is critical data. The trains are threads. The switching stations are Mutexes. Only one thread at a time can control a mutex and any other thread that wants to use a mutex will have to wait until the current thread is done (or it gets tired or trying to use it and goes off to do something else). The basics of using a mutex are straightforward, but to make life easy I wrapped them into a nice little base class as follows.


The cMonitor class





class cMonitor
{
  HANDLE d_mutex;

public: cMonitor() { #if defined( _WIN32 ) && defined( _MT ) // This mutex will help the two threads share their toys. d_mutex = CreateMutex( NULL, false, NULL ); if( d_mutex == NULL ) throw cError( "cMonitor::cMonitor() - Mutex creation failed." ); #endif }

virtual ~cMonitor() { #if defined( _WIN32 ) && defined( _MT ) if( d_mutex != NULL ) { CloseHandle( d_mutex ); d_mutex = NULL; } #endif }

void MutexOn() const { #if defined( _WIN32 ) && defined( _MT ) WaitForSingleObject( d_mutex, INFINITE ); // To be safe... #endif }

void MutexOff() const { #if defined( _WIN32 ) && defined( _MT ) ReleaseMutex( d_mutex ); // To be safe... #endif } };


Since creation and destruction of the mutex are taken care of the moment the class is created, The only thing left to do is use the MutexOn() and MutexOff() functions. How about our previous linked list example?


// Something thread A does
gListProtector.MutexOn();
delete protectedListItem5;
protectedListItem4->SetNext( NULL );
gListProtector.MutexOff();

...

// Something thread B does gListProtector.MutexOn(); for( pList = &protectedList; pList != NULL; pList = pList->Next() ) { // search for some criteria } gListProtector.MutexOff();


in both cases gListProtector is the same instance of cMonitor. What happens is that either thread will wait an INFINITE amount of time (careful!) until the other thread calls cMonitor::MutexOff(). That way nobody steps on anybody's toes. And don't worry - if the same thread calls cMonitor::MutexOn() twice, it won't get stuck (:


Suggestions & comments on feedback


Rather than make a actual gListProtector, I made cMutex a base class of cThread. Then when I derived cThread I made sure that any thread that wanted to use cDerivedFromCThread::someData could only be access it through cDerivedFromCThread::SomeFunction(), which would in turn use cMonitor to guarantee that nobody could screw things up.

Ok, now It's been 3 days since part 1 of this tutorial was posted on the internet and I've already received four emails about it, which is more feedback than I've gotten on anything. Ever. To those people: Thank you!

However: every one of the emails I got went on to suggest "make gsThreadProc a (friend/static member) of cThread" and a few have added "then you could make cThread::ThreadProc protected." I thought I mentioned it in the first part, but let me reiterrate: Notice all those


#if defined( _WIN32 ) && defined( _MT )
  // Blah blah blah
#endif
 


everywhere? If you do NOT turn on the option to build a multithreaded application, all the multithreaded code DISAPPEARS. But perhaps you still want to be able to call cDerivedFromCThread::ThreadProc() - what then? The only way is to leave cThread::ThreadProc() public or make every class that wants to call ThreadProc() a friend. Just because they play nice together doesn't mean they should be friends: friend classes can access protected data, which would make our mutex protected data unsafe.

I would advise that you NOT remove the #if defined(), especially if you want to port your code to another platform, multithreading in Linux, Unix and on Macs is completely different, but the monitor and thread classes shouldn't have to change.

If you are debugging your application and it seems like data that you don't refrence is being modified in curious ways then it's probably because you've written something into funny memory (such as beyond the bounds of an array) and the effect didn't appear until the other thread was called.

Anyhow, that's just about everything I know about multithreading so far. If there's something you'd like me to add or a topic you'd like me to write about, contact me!

Created: Dan Royer aka "Aggrav8d"
Contact: aggravated@bigfoot.com

Global scope variables and functions are like "20 CDs for 1 penny" deals from mail order companies - they sound good but they're almost impossible to get rid of (and most of the time turn into a big headache).


Article Series:
  • Multithreading - Part 1
  • Multithreading - Part 2
  •  

    Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
    Please read our Terms, Conditions, and Privacy information.