Jerry's Tutorials: Turning Functions into Threads, 3 of 3

Jerry's Tutorials: Turning Functions into Threads, 3 of 3


The second error that we will fix in the example code is that the printing from the two threads may become interleaved.  The program is intended to print one line of 50 question marks and to print line of 50 exclamation points.  But when the functions funct1 and funct2 are converted into threads, and when the main program is changed to wait on the threads to finish, then the question marks and the exclamation points can become interleaved.  The function version of the program prints all 50 question marks before printing any of the exclamation points.  The threaded version of the program does not attempt to control whether the question marks or the exclamation points print first.  But it is the case either that all the question marks should print before any of the exclamation points, or vice versa.

The solution for the print interleaving problem is a mutex.  A mutex is a locking mechanism that enables two or more threads to serialize a portion of their processing.  In this case, the printing will be serialized so that only one of the two threads can be printing at the same time.

One purpose for threads is that multiple threads can be executing at the same time, which is most efficacious when there are multiple processor cores available so that the threads can truly be running in parallel.  In a sense, the use of mutexes can defeat temporarily the parallel processing that is one of the chief purposes for using threads.  Nevertheless, judicious use of mutexes is nearly always required with threads to protect sections of code that have to be serialized.

Even though a mutex is a locking mechanism, I don't want to call a mutex a lock because there is something else very akin to a mutex that we are soon going to be calling a lock.  So we will just call a mutex a mutex, and if that seems a bit circular, then so be it.


#include "stdafx.h"
#include <iostream>
#include <boost/thread.hpp>

void funct1();
void funct2();

boost::mutex zzzz;

int main()
{	
    boost::thread xxxx(funct1);
    boost::thread yyyy(funct2);
    xxxx.join();
    yyyy.join();
    return 0;
}

void funct1()
{
    zzzz.lock();
    for (int i = 0; i < 50; i++) std::cout << '?';
    std::cout << std::endl;
    zzzz.unlock();
}

void funct2()
{
    zzzz.lock();
    for (int i = 0; i < 50; i++) std::cout << '!';
    std::cout << std::endl;
    zzzz.unlock();
}
  • A mutex is coded in C++ as a variable.  A mutex variable is instantiated as an object of type boost::mutex.  In this example, the mutex variable is named zzzz.
  • I continue the convention in this example of using variable names for things like thread objects and mutexes that are in no way meaningful, simply to reinforce the fact that there are no semantics associated with the names of the objects involved with threads.  Thus far, the tutorial is surely simple enough that I could have called the mutex that serializes printing something like print_mutex without confusion.  But when we get into locks, I do think that having the nonsense types of names helps to add clarity to the example rather than hindering clarity.
  • We don't really need to know what's inside a mutex, but it may help in understanding to realize that a mutex includes a counter, a pointer, and some flags.
    • The flags are used with a compare and swap instruction as a part of the process of locking and unlocking the mutex.  When there is no contention for the mutex, the compare and swap mechanism enables the mutex to be locked and unlocked in virtually zero time and with no operating system overhead or context switching. 
    • When there is contention, the counter and pointer keep track of how many threads are waiting on the mutex and maintain a queue of the waiting threads. 
  • A mutex must be in scope to all threads that are using it, and a mutex must remain in scope until all threads that are using it are completely finished.  In simple examples such as this one, that means that a mutex such as zzzz usually is given global scope in front of the main function.  It's normally not good practice to have variables with global scope.  In a larger and more realistic program, other measures than global scope would usually be taken to keep a mutex in scope for all the threads that are using it.
  • In addition to data elements such as a counter, a pointer, and flags, a mutex object has access to a number of functions that are methods of the mutex class.  The most commonly used member methods are lock() and unlock().  The use of the mutex object and its member methods lock() and unlock() should be fairly obvious in this example.  Any code that needs to be serialized is preceded by locking the mutex and the code that needs to be serialized is followed by unlocking the mutex.  It's as simple as that.  It is critical, however, not to forget to unlock the mutex at the end of the code that needs to be serialized. 
  • In more realistic programs there can be a danger that a program may throw an exception while holding a mutex.  Throwing an exception in this manner can result in exiting from the code which is holding the mutex without unlocking the mutex.  So there can be more to unlocking a mutex than simply remembering to unlock it.  There are some more sophisticated mechanisms that can be used to deal with these more arcane situations, and we will deal with them further into the tutorial.

Return to Jerry's Home Page

This page last edited on 26 Mar 2016.