Jerry's Tutorials: Boost Locks for C++ Programming, 1 of 1

Jerry's Tutorials: Boost Locks for C++ Programming, 1 of 1


It's not possible to utilize boosts condition variables without using locks.  Were it not for that requirement, I would much prefer just to use mutexes and never to use locks.  But I need to use condition variables, so use locks I must.

A lock is a variable which is instantiated as an object of type boost::lock.  Well, there really isn't a boost class named boost::lock.  Rather, there are several types of locks.  The several types of locks have different names rather than all having the same name with different options.  We will be looking at locks of type boost::lock_guard and of type boost::unique_lock.

Locks do not replace mutexes.  Rather, locks enhance the function of mutexes.  In particular, locks include a pointer to a mutex, and locks include member methods to help manage the mutex to which they point.

The simplest type of lock is of type boost::lock_guard.  It has no options, and there is only one way to use it.  This example will use a lock_guard to serialize two threads rather than using a mutex to serialize two threads.

Scope is hugely important to the way locks work.  Indeed, one of the primary motivations for developing locks was to provide a way to be sure that mutexes are released, even if an exception is thrown after locking a mutex and before unlocking the mutex.

To that end, mutexes continue to require a scope that lasts until the threads are completed, even when mutexes are used along with locks.  But the locks themselves have a scope that is very brief.  Locks are expected to go in and out of scope many times and to stay in scope very briefly.  Which is to say that much or all of the processing performed by locks is performed by their constructors and destructors.


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

void funct1();
void funct2();

boost::mutex zzzz;

int main()  // sample program using lock_guard
{	
    boost::thread xxxx(funct1);
    boost::thread yyyy(funct2);
    xxxx.join();
    yyyy.join();
    return 0;
}

void funct1()
{
    {  // artificially create a scope to contain the lock
    
       // the constructor for the wwww object
       // locks the mutex called zzzz
       boost::lock_guard<boost::mutex> wwww(zzzz);
       for (int i = 0; i < 50; i++) std::cout << '?';
       std::cout << std::endl;
    }  // the lock called wwww goes out of scope at
       //this point and the destructor unlocks the mutex zzzz
}   
    

void funct2()
{
    {  // artificially create a scope to contain the lock
    
       // the constructor for the vvvv object
       // locks the mutex called zzzz
       boost::lock_guard<boost::mutex> vvvv(zzzz);
       for (int i = 0; i < 50; i++) std::cout << '!';
       std::cout << std::endl;
    }  // the lock called vvvv goes out of scope at
       // this point and the destructor unlocks the mutex zzzz
}
  • A lock_guard is coded in C++ as a variable.  A lock_guard variable is instantiated as an object of type boost::lock_guard.  In this example, the lock_guard variable is named vvvv in one function and is named wwww in the other function.  The type is actually coded as boost::lock_guard<boost::mutex> rather than just as boost::lock_guard.  The reason is that there are several kinds of mutexes just as there are several kinds of locks, and lock_guard must be told which kind of mutex it is working with.
  • I continue the convention in this example of using variable names for things like mutex objects and locks 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.
  • Using meaningful names for mutexes in examples may not be too confusing, but I think that using meaningful names for locks in examples can be very confusing.  In particular, many tutorials use examples that give a lock_guard variable the name lock.  The syntax and semantics when the lock_guard variable is instantiated can give the impression to the unwary reader that the identifier lock is the name of a function.  As such, the name lock would have meaning and would appear to be a function which is a member method of the class boost::lock_guard.  For example, many tutorials contain code something like the following.
    boost::lock_guard<boost::mutex> lock(mtx);
    
    If you look deeply enough into C++ syntax and semantics, I suppose it's clear that lock is a variable name and not the name of a function.  The variable is named lock and the lock variable is initialized by its constructor to point to a mutex variable called mtx.  But to a simple country boy like me who has been using the lock() and unlock() functions for years, this code looks like it is using some sort of new-fangled version of the lock() function where mtx is the argument.  Therefore, my use of the identifiers vvvv and wwww for the two instantiations of the lock_guard class are intended to reinforce that these identifiers are variables names that have no meaning instead of being function names that do have a meaning.
  • Of all the various kinds of locks, the lock_guard is the simplest.  It can only be used in one way, and that one way is demonstrated in this example.
    • When it is declared, a lock_guard must specify the name of a mutex in parentheses.  This mutex is passed to the constructor for the mutex object.  The constructor saves the address of the mutex within the lock object, and the constructor locks the mutex.
    • The lock_guard class does not include a lock() function, and a lock_guard cannot be locked after it is instantiated.  It can only be locked as part of being instantiated.  Strictly speaking, it is the mutex that is being locked, not the lock_guard object.  But the principle remains the same in that the locking can only take place during instantiation of the lock_guard object and not afterwards.
    • The lock_guard class does not include an unlock() function.  The only way it can be unlocked is by its destructor.  Which is to say that the only way it can be unlocked is by going out of scope.  Strictly speaking, it is the mutex that is being unlocked, not the lock_guard object.  But the principle remains the same in that the unlocking can only take place when the lock object goes out of scope.
    • There are other types of locks such as boost::unique_lock which can be locked after instantiation and which can be unlocked before going out of scope.  The use of condition variables requires the use of a lock such boost::unique_lock because condition variables require more flexibility than simply locking during instantiation and unlocking when going out of scope.  But I wanted to introduce locks with the simplest kind of lock, and the simplest kind is the lock_guard.
    • Just to repeat ourselves and reinforce an important point: the variables wwww and vvvv are instantiated as objects of type boost::lock_guard.  They have to be two separate variables, they have to be local to the functions in which they are instantiated, and they should have a scope which is as limited as possible.  Indeed, I could have named both variables the same name in their separate functions, and they would have been just as separate as they are with different names.  But both wwww and vvvv have to point to the same mutex, namely they both have to point to zzzz.  And zzzz has to be in scope to both functions for the duration of the time that the functions are running.

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

void funct1();
void funct2();

boost::mutex zzzz;

int main()  // sample program using unique_lock instead of lock_guard
{	
    boost::thread xxxx(funct1);
    boost::thread yyyy(funct2);
    xxxx.join();
    yyyy.join();
    return 0;
}

void funct1()
{
    {  // artificially create a scope to contain the lock
    
       // the constructor for the wwww object
       // locks the mutex called zzzz
       boost::unique_lock<boost::mutex> wwww(zzzz);
       for (int i = 0; i < 50; i++) std::cout << '?';
       std::cout << std::endl;
    }  // the lock called wwww goes out of scope at
       //this point and the destructor unlocks the mutex zzzz
}   
    

void funct2()
{
    {  // artificially create a scope to contain the lock
    
       // the constructor for the vvvv object
       // locks the mutex called zzzz
       boost::unique_lock<boost::mutex> vvvv(zzzz);
       for (int i = 0; i < 50; i++) std::cout << '!';
       std::cout << std::endl;
    }  // the lock called vvvv goes out of scope at
       // this point and the destructor unlocks the mutex zzzz
}
  • Just for completeness, we point out that unique_lock could be used in place of lock_guard in this example.  unique_lock has many more features than lock_guard, and unique_lock is slightly less efficient than lock_guard if all you need is basic locking and unlocking capability.  If used without all its fancy features, the syntax and semantics for unique_lock are identical to the syntax and semantics for lock_guard.
  • I confess to a certain befuddlement with respect to the names that have been chosen for the various kinds of locks.
    • The naming scheme seems inconsistent.  It seems to me that we should have guard_lock and unique_lock, or else we should have lock_guard and lock_unique.  But we don't.  Rather, we have lock_guard and unique_lock.
    • I find myself unable to divine any clues from the names as to what the differences are between the various kinds of locks.  Unless I am missing something really obvious, the names for lock_guard and unique_lock don't provide me with any information at all as to which type of lock performs which functions.

Return to Jerry's Home Page

This page last edited on 26 Mar 2016.