The Boost C++ Libraries


Chapter 8: Interprocess Communication


Table of Contents

This book is licensed under a Creative Commons License.

A new edition of this book is available! It has been published as a print book and can be bought from Barnes and Noble, Amazon and other bookstores. The new edition is up-to-date and based on the Boost C++ Libraries 1.47.0 (released in July 2011). Several chapters have been updated (for example to Boost.Spirit 2.x, Boost.Signals 2 and Boost.Filesystem 3) and many new libraries are covered (for example Boost.CircularBuffer, Boost.Intrusive and Boost.MultiArray). For more information please see the publisher's website XML Press.


8.1 General

Interprocess communication describes mechanisms to exchange data between different applications running on the same computer. This does not include network communication though. If data is required to be exchanged between applications running on different computers connected via network, take a look at Chapter 7, Asynchronous Input and Output which covers the Boost.Asio library.

This chapter presents the Boost.Interprocess library that contains numerous classes providing an abstract layer for operating system specific interprocess communication interfaces. Even though the concepts of interprocess communication are closely related between different operating systems, the interfaces can vary greatly. Boost.Interprocess allows a platform independent access to these functions from C++.

While Boost.Asio can be used for exchanging data between applications running on the same computer as well, the performance using Boost.Interprocess is usually better. Boost.Interprocess actually accesses operating system functions optimized for the data exchange between applications running on the same computer and thus should be the first choice whenever data exchange without a network is required.


8.2 Shared Memory

Shared memory is typically the fastest form of interprocess communication. It provides a memory area that is shared between different applications. One application can write data to the area while another application can read the same data accordingly.

Such a memory area is represented in Boost.Interprocess by the boost::interprocess::shared_memory_object class. To use this class, the boost/interprocess/shared_memory_object.hpp header file needs to be included.

#include <boost/interprocess/shared_memory_object.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write); 
  shdmem.truncate(1024); 
  std::cout << shdmem.get_name() << std::endl; 
  boost::interprocess::offset_t size; 
  if (shdmem.get_size(size)) 
    std::cout << size << std::endl; 
} 

The constructor of boost::interprocess::shared_memory_object expects three parameters. The first one specifies whether the shared memory should be created or just opened. The above example actually does it either way: using boost::interprocess::open_or_create, the shared memory is opened if it already exists, otherwise it will be created.

Opening an existing shared memory assumes that it has been created before. To uniquely identify a shared memory, a name is being assigned accordingly. The second parameter passed to the constructor of boost::interprocess::shared_memory_object specifies that name.

The third and last parameter determines how an application can access the shared memory. The shown application can both read from and write to the shared memory due to the boost::interprocess::read_write value.

After creating an object of type boost::interprocess::shared_memory_object, a corresponding shared memory exists within the operating system. The size of this memory area, however, is initially 0. In order to utilize the area, the truncate() method needs to be called, passing the requested size of the shared memory in bytes. For the above example, the shared memory provides space for 1,024 bytes.

Please note that the truncate() method can only be called if the shared memory has been opened with boost::interprocess::read_write. If not, an exception of type boost::interprocess::interprocess_exception will be thrown.

In order to adjust the size of the shared memory, the truncate() method can be called repeatedly.

After creating a shared memory, methods such as get_name() and get_size() can be used to query the name and the size, respectively.

Since shared memory is used to exchange data between different applications, each application needs to map the shared memory into its address space which is done via the boost::interprocess::mapped_region class.

It may come as a surprise that two classes are actually used in order to access the shared memory. However, the boost::interprocess::mapped_region class can also be used to map different objects into the address space of a particular application. For example, Boost.Interprocess provides a boost::interprocess::file_mapping class which essentially represents a shared memory for a particular file. Thus, an object of type boost::interprocess::file_mapping corresponds to a file. Data written to such an object is automatically saved in the associated physical file. Since boost::interprocess::file_mapping does not load the file completely but rather maps arbitrary parts into the address space using boost::interprocess::mapped_region, it is possible to process files of several gigabytes in size that can not be completely loaded into memory on 32-bit systems otherwise.

#include <boost/interprocess/shared_memory_object.hpp> 
#include <boost/interprocess/mapped_region.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write); 
  shdmem.truncate(1024); 
  boost::interprocess::mapped_region region(shdmem, boost::interprocess::read_write); 
  std::cout << std::hex << "0x" << region.get_address() << std::endl; 
  std::cout << std::dec << region.get_size() << std::endl; 
  boost::interprocess::mapped_region region2(shdmem, boost::interprocess::read_only); 
  std::cout << std::hex << "0x" << region2.get_address() << std::endl; 
  std::cout << std::dec << region2.get_size() << std::endl; 
} 

In order to use the boost::interprocess::mapped_region class, the header boost/interprocess/mapped_region.hpp needs to be included. An object of type boost::interprocess::shared_memory_object must be passed as the first argument to the constructor of boost::interprocess::mapped_region. The second argument determines whether the memory area can be accessed read-only or whether applications are allowed to write to it as well.

The above example creates two objects of type boost::interprocess::mapped_region: The shared memory named Highscore is mapped twice into the address space of the process. The address as well as the size of the mapped memory area is written to the standard output stream using the get_address() and get_size() methods. While get_size() returns 1024 in both cases, the return value of get_address() is different.

The following example uses the mapped memory area to write and read a number.

#include <boost/interprocess/shared_memory_object.hpp> 
#include <boost/interprocess/mapped_region.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write); 
  shdmem.truncate(1024); 
  boost::interprocess::mapped_region region(shdmem, boost::interprocess::read_write); 
  int *i1 = static_cast<int*>(region.get_address()); 
  *i1 = 99; 
  boost::interprocess::mapped_region region2(shdmem, boost::interprocess::read_only); 
  int *i2 = static_cast<int*>(region2.get_address()); 
  std::cout << *i2 << std::endl; 
} 

By means of region, the number 99 is written to the beginning of the shared memory. region2 then accesses the same location of the shared memory to write the number to the standard output stream. Even though region and region2 represent different memory areas within the process as seen by the return value of get_address() in the previous example, the program prints 99 since both memory areas access the same underlying shared memory.

Usually, multiple objects of type boost::interprocess::mapped_region would not be used for the shared memory within the same application. There is actually not much sense accessing the same shared memory using two different memory areas. The above example is only meant for illustration purposes.

In order to delete a particular shared memory, boost::interprocess::shared_memory_object offers the static remove() method which takes the name of the shared memory to be deleted as its argument.

Boost.Interprocess kind of supports the RAII concept known from the chapter about smart pointers using a separate class named boost::interprocess::remove_shared_memory_on_destroy. Its constructor expects the name of an existing shared memory. If an object of this class is destroyed, the contained shared memory is automatically deleted within the destructor.

Please note that the constructor of boost::interprocess::remove_shared_memory_on_destroy does not create or open the shared memory. Therefore, this class is not a typical representative of the RAII concept.

#include <boost/interprocess/shared_memory_object.hpp> 
#include <iostream> 

int main() 
{ 
  bool removed = boost::interprocess::shared_memory_object::remove("Highscore"); 
  std::cout << removed << std::endl; 
} 

If remove() is not being called at all, the shared memory continues to exist even if the application is terminated. Whether or not the shared memory is deleted depends on the underlying operating system. While many Unix operating systems, including Linux, automatically delete a shared memory once the system is restarted, remove() must be called while working with Windows or Mac OS X. Both systems actually store the shared memory as a persistent file which is still available after a restart.

Windows provides a special kind of shared memory that is automatically deleted once the last application using it has been terminated. In order to use it, the boost::interprocess::windows_shared_memory class, defined in boost/interprocess/windows_shared_memory.hpp, is provided.

#include <boost/interprocess/windows_shared_memory.hpp> 
#include <boost/interprocess/mapped_region.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::windows_shared_memory shdmem(boost::interprocess::open_or_create, "Highscore", boost::interprocess::read_write, 1024); 
  boost::interprocess::mapped_region region(shdmem, boost::interprocess::read_write); 
  int *i1 = static_cast<int*>(region.get_address()); 
  *i1 = 99; 
  boost::interprocess::mapped_region region2(shdmem, boost::interprocess::read_only); 
  int *i2 = static_cast<int*>(region2.get_address()); 
  std::cout << *i2 << std::endl; 
} 

Please note that boost::interprocess::windows_shared_memory does not provide a truncate() method. Instead, the size of the shared memory needs to be passed as the fourth argument to the constructor.

Even though the boost::interprocess::windows_shared_memory class is not portable and can only be used within Windows, it is helpful while exchanging data between different applications that use this special kind of shared memory.


8.3 Managed Shared Memory

The previous section introduced the class boost::interprocess::shared_memory_object used to create and manage a shared memory. In practice, this class is hardly used though since it requires to read and write individual bytes from and to the shared memory. Conceptually, C++ rather promotes creating objects of classes and hiding the specifics of where and how they are stored in memory.

Boost.Interprocess provides a concept named managed shared memory through the boost::interprocess::managed_shared_memory class defined in boost/interprocess/managed_shared_memory.hpp. With the aid of this class, objects can be instantiated in a way that the memory required by the individual object is located in shared memory making each object automatically available to other applications accessing the same shared memory.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object::remove("Highscore"); 
  boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024); 
  int *i = managed_shm.construct<int>("Integer")(99); 
  std::cout << *i << std::endl; 
  std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer"); 
  if (p.first) 
    std::cout << *p.first << std::endl; 
} 

The above example opens the shared memory named Highscore with a size of 1,024 bytes. In case it does not exist, it will be automatically created.

While in a regular shared memory individual bytes are being directly accessed in order to read or write data, managed shared memory uses methods such as construct(). This method expects a data type as its template argument - in the example type int is declared. The method itself expects a name to denote the object created in the managed shared memory. The example uses the name 'Integer'.

Since construct() returns a proxy object, arguments can be passed to it in order to initialize the created object. The syntax looks like a call to a constructor. This ensures that objects not only can be created in a managed shared memory but also can be initialized as desired.

To access a particular object in the managed shared memory, the find() method is used. By passing the name of the object to find, find() returns either a pointer to the particular object or 0 in case no object with the given name was found.

As seen in the above example, find() actually returns an object of type std::pair. The pointer to the object is provided as the first property. What is provided as the second property though?

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object::remove("Highscore"); 
  boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024); 
  int *i = managed_shm.construct<int>("Integer")[10](99); 
  std::cout << *i << std::endl; 
  std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer"); 
  if (p.first) 
  { 
    std::cout << *p.first << std::endl; 
    std::cout << p.second << std::endl; 
  } 
} 

This time, an array for ten elements of type int is created by providing the value 10 enclosed by square brackets after the call to construct(). The same 10 is written to the standard output stream using the second property. Using this property, objects returned by the find() method can be distinguished between single objects and array objects. For the former, second is set to 1 while for the latter it will specify the number of elements in the array.

Please note that all ten elements within the array are initialized with the value 99. It is not possible to initialize individual elements with different values.

construct() will fail if there exists an object with the given name in the managed shared memory already. In this case, construct() returns 0. If the existing object should be reused in case it already exists, the find_or_construct() method can be used which returns a pointer to the existing object if found. In this case, no initialization happens.

There are other cases that cause construct() to fail as shown in the following example.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <iostream> 

int main() 
{ 
  try 
  { 
    boost::interprocess::shared_memory_object::remove("Highscore"); 
    boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024); 
    int *i = managed_shm.construct<int>("Integer")[4096](99); 
  } 
  catch (boost::interprocess::bad_alloc &ex) 
  { 
    std::cerr << ex.what() << std::endl; 
  } 
} 

The application tries to create an array of type int containing 4,096 elements. The managed shared memory, however, only contains 1,024 bytes. Therefore, the requested memory cannot be provided by the shared memory causing an exception of type boost::interprocess::bad_alloc to be thrown.

Once objects have been created in a managed shared memory, they can be deleted using the destroy() method.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object::remove("Highscore"); 
  boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024); 
  int *i = managed_shm.find_or_construct<int>("Integer")(99); 
  std::cout << *i << std::endl; 
  managed_shm.destroy<int>("Integer"); 
  std::pair<int*, std::size_t> p = managed_shm.find<int>("Integer"); 
  std::cout << p.first << std::endl; 
} 

As its single argument, the name of the object to be deleted is passed to destroy(). If required, the return value of type bool can be checked to verify whether the given object has been found and deleted successfully. Since an object will always be deleted if found, the return value of false indicates that no object with the given name was found.

Besides destroy(), another method named destroy_ptr() is provided which can be used to pass a pointer to an object in the managed shared memory. It also can be used to delete arrays.

Since managed shared memory makes it fairly easy to store objects shared between different applications, it seems natural to use the containers from the C++ Standard Template Library as well. These containers allocate required memory independently using new though. In order to use these containers in managed shared memory, they need to be advised to rather allocate the memory in the shared memory.

Unfortunately, many implementations of the C++ Standard Template Library are not flexible enough to use the provided containers such as std::string or std::list together with Boost.Interprocess. One example for such an implementation is the one that ships with Microsoft Visual Studio 2008.

In order to allow developers the possibility to use the containers known from the C++ standard, Boost.Interprocess offers more flexible implementations of them in the namespace boost::interprocess. For example, boost::interprocess::string acts exactly as its C++ counterpart std::string with the advantage that its objects can be safely stored in a managed shared memory.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <boost/interprocess/allocators/allocator.hpp> 
#include <boost/interprocess/containers/string.hpp> 
#include <iostream> 

int main() 
{ 
  boost::interprocess::shared_memory_object::remove("Highscore"); 
  boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024); 
  typedef boost::interprocess::allocator<char, boost::interprocess::managed_shared_memory::segment_manager> CharAllocator; 
  typedef boost::interprocess::basic_string<char, std::char_traits<char>, CharAllocator> string; 
  string *s = managed_shm.find_or_construct<string>("String")("Hello!", managed_shm.get_segment_manager()); 
  s->insert(5, ", world"); 
  std::cout << *s << std::endl; 
} 

To create a string that will allocate required memory within the same managed shared memory it resides, a corresponding data type must be defined in order to use a different allocator, provided by Boost.Interprocess, rather than the default allocator provided by the C++ standard.

For this purpose, Boost.Interprocess offers the class boost::interprocess::allocator defined in boost/interprocess/allocators/allocator.hpp. Using this class, an allocator can be created that internally uses the so-called segment manager of the managed shared memory. The segment manager is responsible for the management of the memory within a managed shared memory. Using the newly created allocator, a corresponding data type for the string can be defined. As indicated above, it uses boost::interprocess::basic_string rather than std::basic_string. The new data type - in the example simply named string - is based on boost::interprocess::basic_string and accesses the segment manager via its allocator. In order to let the particular instance of string, created by a call to find_or_construct(), know which segment manager it should access, a pointer to the corresponding segment manager is passed as the second argument to the constructor.

Alongside boost::interprocess::string, Boost.Interprocess provides implementations for many other containers known from the C++ standard. For example, boost::interprocess::vector and boost::interprocess::map defined in boost/interprocess/containers/vector.hpp and boost/interprocess/containers/map.hpp, respectively.

Whenever the same managed shared memory is accessed from different applications, operations such as creating, finding and destroying objects are automatically synchronized. If two applications try to create objects with different names in the managed shared memory, the access is serialized accordingly. In order to execute multiple operations at once without being interrupted by operations from a different application, the atomic_func() method can be used.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <boost/bind.hpp> 
#include <iostream> 

void construct_objects(boost::interprocess::managed_shared_memory &managed_shm) 
{ 
  managed_shm.construct<int>("Integer")(99); 
  managed_shm.construct<float>("Float")(3.14); 
} 

int main() 
{ 
  boost::interprocess::shared_memory_object::remove("Highscore"); 
  boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "Highscore", 1024); 
  managed_shm.atomic_func(boost::bind(construct_objects, boost::ref(managed_shm))); 
  std::cout << *managed_shm.find<int>("Integer").first << std::endl; 
  std::cout << *managed_shm.find<float>("Float").first << std::endl; 
} 

atomic_func() expects a function, taking no arguments and not returning any value, as its single argument. The passed function will be called in a fashion that ensures exclusive access to the managed shared memory - but only for operations such as creating, finding or deleting objects. If another application has a pointer to an object within the managed shared memory, it can access and modify this object using its pointer.

Boost.Interprocess can also be used to synchronize object access. Since Boost.Interprocess does not know who can access the individual objects at what point in time, the synchronization needs to be explicitly stated. The classes provided for synchronization are introduced in the following section.


8.4 Synchronization

Boost.Interprocess allows multiple applications to use a shared memory concurrently. Since shared memory is by definition "shared" between these applications, Boost.Interprocess needs to support some form of synchronization.

While thinking about synchronization, the library Boost.Thread certainly comes to mind. As can be seen in Chapter 6, Multithreading, Boost.Thread indeed provides different concepts such as mutex objects and conditional variables to synchronize threads. Unfortunately, these classes can only be used to synchronize threads within the same application; they do not support the synchronization of different applications. Since the challenge in both cases is the same, the concepts are no different.

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

Boost.Interprocess offers two kinds of synchronization objects: Anonymous objects are directly stored in the shared memory which makes them automatically available to all applications. Named objects are managed by the operating system and thus are not stored in the shared memory. They can be referenced by applications via their name.

The following example creates and uses a named mutex object by using the boost::interprocess::named_mutex class defined in boost/interprocess/sync/named_mutex.hpp.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <boost/interprocess/sync/named_mutex.hpp> 
#include <iostream> 

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

Besides a parameter specifying whether the mutex object should be created or opened, the constructor of boost::interprocess::named_mutex expects a name for it. Every application that knows that name is able to open the same mutex object. In order to get access to the data in the shared memory, the application needs to take ownership of the mutex object by calling the lock() method. Since mutex objects can only be owned by one application at any time, another application may need to wait until the mutex object has been released using the unlock() method by the first application. Once an application obtained ownership of a mutex object, it has exclusive access to the resource it guards. In the above example the resource is a variable of type int that is incremented and written to the standard output stream.

If the application is started multiple times, every instance will print a value incremented by 1 compared to the previous value. Thanks to the mutex object, the access to the shared memory and the variable itself is synchronized between the different applications.

The following application uses an anonymous mutex object of type boost::interprocess::interprocess_mutex defined in boost/interprocess/sync/interprocess_mutex.hpp. In order to be accessible for all applications, it is stored in the shared memory.

#include <boost/interprocess/managed_shared_memory.hpp> 
#include <boost/interprocess/sync/interprocess_mutex.hpp> 
#include <iostream> 

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

The application behaves exactly like the previous one. The only difference is the mutex object which is now stored directly in the shared memory using the construct() or find_or_construct() method of the boost::interprocess::managed_shared_memory class.

Besides the lock() method, both boost::interprocess::named_mutex and boost::interprocess::interprocess_mutex provide the try_lock() and timed_lock() methods. They behave exactly like their counterparts provided by the mutex classes of Boost.Thread.

In case recursive mutex objects are required, Boost.Interprocess offers the two classes boost::interprocess::named_recursive_mutex and boost::interprocess::interprocess_recursive_mutex.

While mutex objects guarantee exclusive access to a shared resource, conditional variables control who has to have exclusive access at what time. In general, the conditional variables provided by Boost.Interprocess work the same way as the ones provided by Boost.Thread. They also have very similar interfaces which makes users of Boost.Thread feel immediately at home when using these variables in Boost.Interprocess.

#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> 

int main() 
{ 
  boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024); 
  int *i = managed_shm.find_or_construct<int>("Integer")(0); 
  boost::interprocess::named_mutex named_mtx(boost::interprocess::open_or_create, "mtx"); 
  boost::interprocess::named_condition named_cnd(boost::interprocess::open_or_create, "cnd"); 
  boost::interprocess::scoped_lock<boost::interprocess::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(); 
  boost::interprocess::shared_memory_object::remove("shm"); 
  boost::interprocess::named_mutex::remove("mtx"); 
  boost::interprocess::named_condition::remove("cnd"); 
} 

The example uses a conditional variable of type boost::interprocess::named_condition defined in boost/interprocess/sync/named_condition.hpp. Since 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 that is stored in shared memory. While 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 has been incremented by 1, the wait() method of the conditional variable named_cnd is called. A so-called lock - in the example the variable lock - is passed to this method. The lock has the same meaning as it does in Boost.Thread: It is based on the RAII concept by taking ownership of a mutex object inside the constructor and releasing it inside the destructor.

The lock is created before the while loop and thus takes ownership of the mutex object for the complete execution of the application. However, if passed to the wait() method as an argument, it is automatically released.

Conditional variables are used to wait for a signal indicating that the wait is now over. The synchronization is controlled by the wait() and notify_all() methods. If an application calls the wait() method, the ownership of the corresponding mutex object is released before it waits until the notify_all() method is called for the same conditional variable.

If started, the application does not seem to do much: While the variable is incremented from 0 to 1 within the while loop, the application then waits for a signal using the wait() method. In order to provide the signal, the application needs to be started a second time.

The second instance of the application is going to try to take ownership of the same mutex object before entering the while loop. This succeeds since the first instance of the application released ownership of the mutex object by calling wait(). Since the variable has been incremented once, the second instance will now execute the else branch of the if expression which will write the current value to the standard output stream before incrementing it by 1.

Now the second instance also calls the wait() method. However, before it does, it calls the notify_all() method which is important in order to have the two instances cooperate correctly. The first instance is notified and tries to take ownership of the mutex object again, which is still owned by the second instance though. Since the second instance calls the wait() method right after notify_all(), which automatically releases the ownership, the first instance can take the ownership at that point.

Both instances alternate, incrementing the variable in the shared memory. Only one instance writes the value to the standard output stream though. As soon as the variable reaches the value 10, the while loop is finished. In order to have the other instance not waiting for a signal forever, notify_all() is called one more time after the loop. Before terminating, the shared memory, the mutex object as well as the conditional variable is destroyed.

Just like there are two types of mutex objects - an anonymous type that must be stored in shared memory as well as a named type - there also exists two type of conditional variables. The previous example is now rewritten using an anonymous conditional variable instead.

#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> 

int main() 
{ 
  try 
  { 
    boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024); 
    int *i = managed_shm.find_or_construct<int>("Integer")(0); 
    boost::interprocess::interprocess_mutex *mtx = managed_shm.find_or_construct<boost::interprocess::interprocess_mutex>("mtx")(); 
    boost::interprocess::interprocess_condition *cnd = managed_shm.find_or_construct<boost::interprocess::interprocess_condition>("cnd")(); 
    boost::interprocess::scoped_lock<boost::interprocess::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(); 
  } 
  catch (...) 
  { 
  } 
  boost::interprocess::shared_memory_object::remove("shm"); 
} 

The application works exactly like the previous one and thus needs to be started twice as well in order to increment the variable ten times. The differences between the two examples are minimal. Whether anonymous or named conditional variables are used is essentially irrelevant.

Besides mutex objects and conditional variables, Boost.Interprocess also supports so-called semaphores and file locks. Semaphores behave similar to conditional variables with the exception that they do not distinguish between two states but rather are based upon a counter. File locks on the other hand behave like mutex objects although they are not about objects in memory but rather about files in the file system.

Just like Boost.Thread distinguishes between different types of mutex objects and locks, Boost.Interprocess also provides several mutex objects and locks. For example, mutex objects that not only can be owned exclusive but also non-exclusive. This is helpful if multiple applications need to read data simultaneously but write the data exclusively. Different classes for locks are available to apply the RAII concept to the individual mutex objects.

Please note that names should be unique unless anonymous synchronization objects are used. Even though mutex objects and conditional variables are objects based on different classes, this may not necessarily hold true for the operating system dependent interfaces prescinded by Boost.Interprocess. In Windows, the same operating system functions are used for both mutex objects and conditional variables. If the same name is used for both these objects, the application will not behave correctly in Windows.


8.5 Exercises

You can buy solutions to all exercises in this book as a ZIP file.

  1. Create a client/server application that communicates via shared memory. The name of a file should be passed as a command line parameter to the client application. This file should be sent to the server application via shared memory where it is saved locally in the same directory from which the server application was started.