Home arrow C++ arrow Page 8 - Multithreading in C++
C++

Multithreading in C++


Multithreading is growing in importance in modern programming for a variety of reasons, not the least of which being that Windows supports multithreading. While C++ does not feature built-in support for multithreading, it can be used to created multithreaded programs, which is the subject of this article. It is taken from chapter three of The Art of C++, written by Herbert Schildt (McGraw-Hill/Osborne, 2004; ISBN: 0072255129).

Author Info:
By: McGraw-Hill/Osborne
Rating: 5 stars5 stars5 stars5 stars5 stars / 229
July 14, 2005
TABLE OF CONTENTS:
  1. · Multithreading in C++
  2. · An Overview of the Windows Thread Functions
  3. · Priority Classes
  4. · The Windows Synchronization Objects
  5. · The Thread Control Panel
  6. · A Closer Look at the Thread Control Panel
  7. · Demonstrating the Control Panel
  8. · A Multithreaded Garbage Collector
  9. · Synchronizing Access to gclist
  10. · The Entire Multithreaded Garbage Collector
  11. · Using the Multithreaded Garbage Collector

print this article
SEARCH DEVARTICLES

Multithreading in C++ - A Multithreaded Garbage Collector
(Page 8 of 11 )

Although controlling threads using the thread control panel is useful when developing multithreaded programs, ultimately it is using threads that makes them important. Toward this end, this chapter shows a multithreaded version of the GCPtr garbage collector class originally developed in Chapter 2. Recall that the version of GCPtr shown in Chapter 2 collected unused memory each time a GCPtr object went out of scope. Although this approach is fine for some applications, often a better alternative is have the garbage collector run as a background task, recycling memory whenever free CPU cycles are available. The implementation developed here is designed for Windows, but the same basic techniques apply to other multithreaded environments.

To convert GCPtr into a background task is actually fairly easy, but it does involve a number of changes. Here are the main ones:

  1. Member variables that support the thread must be added to GCPtr. These variables include the thread handle, the mutex handle, and an instance counter that keeps track of the number of GCPtr objects in existence.

  2. The constructor for GCPtr must begin the garbage collection thread. The constructor must also create the mutex that controls synchronization. This must happen only once, when the first GCPtr object is created.

  3. Another exception must be defined that will be used to indicate a time-out condition.

  4. The GCPtr destructor must no longer call collect( ). Garbage collection is handled by the garbage collection thread.

  5. A function called gc( ) that serves as the thread entry point for the garbage collector must be defined.

  6. A function called isRunning( ) must be defined. It returns true if the garbage collection is in use.

  7. The member functions of GCPtr that access the garbage collection list contained in gclist must be synchronized so that only one thread at a time can access the list.

The following sections show the changes.

The Additional Member Variables

The multithreaded version of GCPtr requires that the following member variables be added:

// These support multithreading.
unsigned tid; // thread id
static HANDLE hThrd;    // thread handle
static HANDLE hMutex;   // handle of mutex
static int instCount;   // counter of GCPtr objects

The ID of the thread used by the garbage collector is stored in tid. This member is unused except in the call to _beginthreadex( ). The handle to the thread is stored in hThrd. The handle of the mutex used to synchronize access to GCPtr is stored in hMutex. A count of GCPtr objects in existence is maintained in instCount. The last three are static because they are shared by all instances of GCPtr. They are defined like this, outside of GCPtr:

template <class T, int size>
  int GCPtr<T, size>::instCount = 0;
template <class T, int size>
  HANDLE GCPtr<T, size>::hMutex = 0;
template <class T, int size>
  HANDLE GCPtr<T, size>::hThrd = 0;

The Multithreaded GCPtr Constructor

In addition to its original duties, the multithreaded GCPtr( ) must create the mutex, start the garbage collector thread, and update the instance counter. Here is the updated version:

// Construct both initialized and uninitialized objects. GCPtr(T *t=NULL) {
  // When first object is created, create the mutex
  // and register shutdown().
  if(hMutex==0) {
    hMutex = CreateMutex(NULL, 0, NULL);
    atexit(shutdown);
  }
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
    throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(t);
  // If t is already in gclist, then
  // increment its reference count.
  // Otherwise, add it to the list.
  if(p != gclist.end())
    p->refcount++; // increment ref count
  else {
    // Create and store this entry.
    GCInfo<T> gcObj(t, size);
    gclist.push_front(gcObj);
  }
  addr = t;
  arraySize = size;
  if(size > 0) isArray = true;
  else isArray = false;
  // Increment instance counter for each new object.
  instCount++;
  // If the garbage collection thread is not
  // currently running, start it running.
  if(hThrd==0) {
    hThrd = (HANDLE) _beginthreadex(NULL, 0, gc,
            (void *) 0, 0, (unsigned *) &tid);
    // For some applications, it will be better
    // to lower the priority of the garbage collector
    // as shown here:
    //
    // SetThreadPriority(hThrd,
    //                   THREAD_PRIORITY_BELOW_NORMAL);
  }
  ReleaseMutex(hMutex);
}

Letís examine this code closely. First, if hMutex is zero, it means that this is the first GCPtr object to be created and no mutex has yet been created for the garbage collector. If this is the case, the mutex is created and its handle is assigned to hMutex. At the same time, the function shutdown( ) is registered as a termination function by calling atexit( ).

It is important to note that in the multithreaded garbage collector, shutdown( ) serves two purposes. First, as in the original version of GCPtr, shutdown( ) frees any unused memory that has not been released because of a circular reference. Second, when a program using the multithreaded garbage collector ends, it stops the garbage collection thread. This means that there might still be dynamically allocated objects that havenít been freed. This is important because these objects might have destructors that need to be called. Because shutdown( ) releases all remaining objects, it also releases these objects.

Next, the mutex is acquired by calling WaitForSingleObject( ). This is necessary to prevent two threads from accessing gclist at the same time. Once the mutex has been acquired, a search of gclist is made, looking for any preexisting entry that matches the address in t. If one is found, its reference count is incremented. If no preexising entry matches t, a new GCInfo object is created that contains this address, and this object is added to gclist. Then, addr, arraySize, and isArray are set. These actions are the same as in the original version of GCPtr.

Next, instCount is incremented. Recall that instCount is initialized to zero. Incrementing it each time an object is created keeps track of how many GCPtr objects are in existence. As long as this count is above zero, the garbage collector will continue to execute.

Next, if hThrd is zero (as it is initially), then no thread has yet been created for the garbage collector. In this case, _beginthreadex( ) is called to begin the thread. A handle to the thread is then assigned to hThrd. The thread entry function is called gc( ), and it is examined shortly.

Finally, the mutex is released and the constructor returns. It is important to point out that each call to WaitForSingleObject( ) must be balanced by a call to ReleaseMutex( ), as shown in the GCPtr constructor. Failure to release the mutex will cause deadlock.

The TimeOutExc Exception

As you probably noticed in the code for GCPtr( ) described in the preceding section, if the mutex cannot be acquired after 10 seconds, then a TimeOutExc is thrown. Frankly, 10 seconds is a very long time, so a time-out shouldnít ever happen unless something disrupts the task scheduler of the operating system. However, in the event it does occur, your application code may want to catch this exception. The TimeOutExc class is shown here:

// Exception thrown when a time-out occurs
// when waiting for access to hMutex.
//
class TimeOutExc {
  // Add functionality if needed by your application.
};

Notice that it contains no members. Its existence as a unique type is sufficient for the purposes of this chapter. Of course, you can add functionality if desired.

The Multithreaded GCPtr Destructor

Unlike the single-threaded version of the GCPtr destructor, the multithreaded version of ~GCPtr( ) does not call collect( ). Instead, it simply decrements the reference count of the memory pointed to by the GCPtr that is going out of scope. The actual collection of garbage (if any exists) is handled by the garbage collection thread. The destructor also decrements the instance counter, instCount.

The multithreaded version of ~GCPtr( ) is shown here:

// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
  if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
    throw TimeOutExc();
  list<GCInfo<T> >::iterator p;
  p = findPtrInfo(addr);
  if(p->refcount) p->refcount--; // decrement ref count
  // Decrement instance counter for each object
  // that is destroyed.
  instCount--;
  ReleaseMutex(hMutex);
}

The gc( ) Function

The entry function for the garbage collector is called gc( ), and it is shown here:

// Entry point for garbage collector thread.
template <class T, int size>
unsigned __stdcall GCPtr<T, size>::gc(void * param) {
  #ifdef DISPLAY
    cout << "Garbage collection started.\n";
  #endif
  while(isRunning()) {
      collect();
  }
  collect(); // collect garbage on way out
  // Release and reset the thread handle so
  // that the garbage collection thread can
  // be restarted if necessary.
  CloseHandle(hThrd);
  hThrd = 0;
  #ifdef DISPLAY
    cout << "Garbage collection terminated for "
         << typeid(T).name() << "\n";
  #endif
  return 0;
}

The gc( ) function is quite simple: it runs as long as the garbage collector is in use. The isRunning( ) function returns true if instCount is greater than zero (which means that the garbage collector is still needed) and false otherwise. Inside the loop, collect( ) is called continuously. This approach is suitable for demonstrating the multithreaded garbage collector, but it is probably too inefficient for real-world use. You might want to experiment with calling collect( ) less often, such as only when memory runs low. You could also experiment by calling the Windows API function Sleep( ) after each call to collect( ). Sleep( ) pauses the execution of the calling thread for a specified number of milliseconds. While sleeping, a thread does not consume CPU time.

When isRunning( ) returns false, the loop ends, causing gc( ) to eventually end, which stops the garbage collection thread. Because of the multithreading, it is possible that there will still be an entry on gclist that has not yet been freed even though isRunning( ) returns false. To handle this case, a final call to collect( ) is made before gc( ) ends.

Finally, the thread handle is released via a call to the Windows API function CloseHandle( ), and its value is set to zero. Setting hThrd to zero enables the GCPtr constructor to restart the thread if later in the program new GCPtr objects are created.

The isRunning( ) Function

The isRunning( ) function is shown here:

// Returns true if the collector is still in use.
static bool isRunning() { return instCount > 0; }

It simply compares instCount to zero. As long as instCount is greater than 0, at least one GCPtr pointer is still in existence and the garbage collector is still needed.


blog comments powered by Disqus
C++ ARTICLES

- Intel Threading Building Blocks
- Threading Building Blocks with C++
- Video Memory Programming in Text Mode
- More Tricks to Gain Speed in Programming Con...
- Easy and Efficient Programming for Contests
- Preparing For Programming Contests
- Programming Contests: Why Bother?
- Polymorphism in C++
- Overview of Virtual Functions
- Inheritance in C++
- Extending the Basic Streams in C++
- Using Stringstreams in C++
- Custom Stream Manipulation in C++
- General Stream Manipulation in C++
- Serialize Your Class into Streams in C++

Watch our Tech Videos 
Dev Articles Forums 
 RSS  Articles
 RSS  Forums
 RSS  All Feeds
Write For Us 
Weekly Newsletter
 
Developer Updates  
Free Website Content 
Contact Us 
Site Map 
Privacy Policy 
Support 

Developer Shed Affiliates

 




© 2003-2017 by Developer Shed. All rights reserved. DS Cluster - Follow our Sitemap
Popular Web Development Topics
All Web Development Tutorials