The Boost C++ Libraries

Synchronization

Boost.Interprocess allows multiple processes to use shared memory concurrently. Because shared memory is, by definition, shared between processes, Boost.Interprocess needs to support some kind of synchronization.

Thinking about synchronization, classes from the C++11 standard library or Boost.Thread come to mind. But these classes can only be used to synchronize threads within the same process; they do not support synchronization of different processes. However, since the challenge in both cases is the same, the concepts are also the same.

While synchronization objects such as mutexes and condition variables reside in the same address space in multithreaded applications, and therefore are available to all threads, the challenge with shared memory is that independent processes need to share these objects. For example, if one process creates a mutex, it somehow needs to be accessible from a different process.

Boost.Interprocess provides two kinds of synchronization objects: anonymous objects are directly stored in the shared memory, which makes them automatically available to all processes. Named objects are managed by the operating system, are not stored in the shared memory, and can be referenced from programs by name.

Example 33.12. Using a named mutex with boost::interprocess::named_mutex
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")();
  named_mutex named_mtx{open_or_create, "mtx"};
  named_mtx.lock();
  ++(*i);
  std::cout << *i << '\n';
  named_mtx.unlock();
}

Example 33.12 creates and uses a named mutex using the class boost::interprocess::named_mutex, which is defined in boost/interprocess/sync/named_mutex.hpp.

The constructor of boost::interprocess::named_mutex expects a parameter specifying whether the mutex should be created or opened and a name for the mutex. Every process that knows the name can open the same mutex. To access the data in shared memory, the program needs to take ownership of the mutex by calling the member function lock(). Because mutexes can only be owned by one process at a time, another process may need to wait until the mutex has been released by unlock(). Once a process takes ownership of a mutex, it has exclusive access to the resource the mutex guards. In Example 33.12, the resource is a variable of type int that is incremented and written to the standard output stream.

If the sample program is started multiple times, each instance will print a value incremented by 1 compared to the previous value. Thanks to the mutex, access to the shared memory and the variable itself is synchronized between different processes.

Example 33.13. Using an anonymous mutex with interprocess_mutex
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")();
  interprocess_mutex *mtx =
    managed_shm.find_or_construct<interprocess_mutex>("mtx")();
  mtx->lock();
  ++(*i);
  std::cout << *i << '\n';
  mtx->unlock();
}

Example 33.13 uses an anonymous mutex of type boost::interprocess::interprocess_mutex, which is defined in boost/interprocess/sync/interprocess_mutex.hpp. In order for the mutex to be accessible for all processes, it is stored in the shared memory.

Example 33.13 behaves exactly like the previous one. The only difference is the mutex, which is now stored directly in shared memory. This can be done with the member functions construct() or find_or_construct() from the class boost::interprocess::managed_shared_memory.

In addition to lock(), both boost::interprocess::named_mutex and boost::interprocess::interprocess_mutex provide the member functions try_lock() and timed_lock(). They behave exactly like their counterparts in the standard library and Boost.Thread. If recursive mutexes are required, Boost.Interprocess provides two classes: boost::interprocess::named_recursive_mutex and boost::interprocess::interprocess_recursive_mutex.

While mutexes guarantee exclusive access to a shared resource, condition variables control who has exclusive access at what time. In general, the condition variables provided by Boost.Interprocess work like the ones provided by the C++11 standard library and Boost.Thread. They have similar interfaces, which makes users of these libraries feel immediately at home when using these variables in Boost.Interprocess.

Example 33.14. Using a named condition with boost::interprocess::named_condition
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/named_mutex.hpp>
#include <boost/interprocess/sync/named_condition.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")(0);
  named_mutex named_mtx{open_or_create, "mtx"};
  named_condition named_cnd{open_or_create, "cnd"};
  scoped_lock<named_mutex> lock{named_mtx};
  while (*i < 10)
  {
    if (*i % 2 == 0)
    {
      ++(*i);
      named_cnd.notify_all();
      named_cnd.wait(lock);
    }
    else
    {
      std::cout << *i << std::endl;
      ++(*i);
      named_cnd.notify_all();
      named_cnd.wait(lock);
    }
  }
  named_cnd.notify_all();
  shared_memory_object::remove("shm");
  named_mutex::remove("mtx");
  named_condition::remove("cnd");
}

Example 33.14 uses a condition variable of type boost::interprocess::named_condition, which is defined in boost/interprocess/sync/named_condition.hpp. Because it is a named variable, it does not need to be stored in shared memory.

The application uses a while loop to increment a variable of type int, which is stored in shared memory. Although the variable is incremented with each iteration of the loop, it will only be written to the standard output stream with every second iteration – only odd numbers are written.

Every time the variable is incremented by 1, the member function wait() of the condition variable named_cnd is called. A lock – in Example 33.14, the variable named lock – is passed to this member function. This is based on the RAII idiom of taking ownership of a mutex inside the constructor and releasing it inside the destructor.

The lock is created before the while loop and takes ownership of the mutex for the entire execution of the program. However, if passed to wait() as a parameter, the lock is automatically released.

Condition variables are used to wait for a signal indicating that the wait is over. Synchronization is controlled by the member functions wait() and notify_all(). When a program calls wait(), ownership of the corresponding mutex is released. The program then waits until notify_all() is called on the same condition variable.

When started, Example 33.14 does not seem to do much. After the variable i is incremented from 0 to 1 within the while loop, the program waits for a signal by calling wait(). In order to fire the signal, a second instance of the program needs to be started.

The second instance tries to take ownership of the same mutex before entering the while loop. This succeeds since the first instance released the mutex by calling wait(). Because the variable has been incremented once, the second instance executes the else branch of the if expression and writes the current value to the standard output stream. Then the value is incremented by 1.

Now the second instance also calls wait(). However, before it does, it calls notify_all(), which ensures that the two instances cooperate correctly. The first instance is notified and tries to take ownership of the mutex again, which is still owned by the second instance. However, because the second instance calls wait() right after calling notify_all(), which automatically releases ownership, the first instance will take ownership at that point.

Both instances alternate, incrementing the variable in the shared memory. However, only one instance writes the value to the standard output stream. As soon as the variable reaches the value 10, the while loop is finished. In order to avoid having the other instance wait for a signal forever, notify_all() is called one more time after the loop. Before terminating, the shared memory, the mutex, and the condition variable are destroyed.

Just as there are two types of mutexes – an anonymous type that must be stored in shared memory and a named type – there are also two types of condition variables. Example 33.15 is a rewrite of the previous example using an anonymous condition variable.

Example 33.15. Using an anonymous condition with interprocess_condition
#include <boost/interprocess/managed_shared_memory.hpp>
#include <boost/interprocess/sync/interprocess_mutex.hpp>
#include <boost/interprocess/sync/interprocess_condition.hpp>
#include <boost/interprocess/sync/scoped_lock.hpp>
#include <iostream>

using namespace boost::interprocess;

int main()
{
  managed_shared_memory managed_shm{open_or_create, "shm", 1024};
  int *i = managed_shm.find_or_construct<int>("Integer")(0);
  interprocess_mutex *mtx =
    managed_shm.find_or_construct<interprocess_mutex>("mtx")();
  interprocess_condition *cnd =
    managed_shm.find_or_construct<interprocess_condition>("cnd")();
  scoped_lock<interprocess_mutex> lock{*mtx};
  while (*i < 10)
  {
    if (*i % 2 == 0)
    {
      ++(*i);
      cnd->notify_all();
      cnd->wait(lock);
    }
    else
    {
      std::cout << *i << std::endl;
      ++(*i);
      cnd->notify_all();
      cnd->wait(lock);
    }
  }
  cnd->notify_all();
  shared_memory_object::remove("shm");
}

Example 33.15 works exactly like the previous one and also needs to be started twice to increment the int variable ten times.

Besides mutexes and condition variables, Boost.Interprocess also supports semaphores and file locks. Semaphores are similar to condition variables except they do not distinguish between two states; instead, they are based on a counter. File locks behave like mutexes, except they are used with files on a hard drive, rather than objects in memory.

In the same way that the C++11 standard library and Boost.Thread distinguish between different types of mutexes and locks, Boost.Interprocess provides several mutexes and locks. For example, mutexes can be owned exclusively or non-exclusively. This is helpful if multiple processes need to read data simultaneously since an exclusive mutex is only required to write data. Different classes for locks are available to apply the RAII idiom to individual mutexes.

Names should be unique unless anonymous synchronization objects are used. Even though mutexes and condition variables are objects based on different classes, this may not necessarily hold true for the operating system dependent interfaces wrapped by Boost.Interprocess. On Windows, the same operating system functions are used for both mutexes and condition variables. If the same name is used for two objects, one of each type, the program will not behave correctly on Windows.

Exercise

Create a client and a server which communicate via shared memory. When the client is started, a filename should be passed as a command line option. The client should send the file to the server. The server should save the file in the current working directory.