8.4. 同步

Boost.Interprocess 允许多个应用程序并发使用共享内存。 由于共享内存被定义为在应用程序之间“共享”,所以 Boost.Interprocess 需要支持一些同步方式。

当考虑到同步的时候,Boost.Thread 当然浮现在脑海里。 正如在 第 6 章 多线程 所见,Boost.Thread 确实提供了各种概念,如互斥对象和条件变量来同步线程。 可惜的是,这些类只能用来同步同一个应用程序内的线程,它们不支持同步不同的应用程序。 由于二者面临的问题相同,所以在概念上没有什么差别。

当诸如互斥对象和条件变量等同步对象位于一个多线程的应用程序的同一地址空间内时,当然它们对于所有线程都是可以访问的,而在共享内存方面的问题是不同的应用程序需要在彼此之间正确地共享这些对象。 例如,如果一个应用程序创建一个互斥对象,它有时候需要从另外一个应用程序访问此对象。

Boost.Interprocess 提供了两种同步对象,匿名对象被直接存储在共享内存上,这使得他们自动对所有应用程序可用。 命名对象由操作系统管理,所以它们不存储在共享内存上,它们可以被应用程序通过名称访问。

接下来的例子通过 boost::interprocess::named_mutex 创建并使用一个命名互斥对象,此类定义在 boost/interprocess/sync/named_mutex.hpp 文件中。

  1. #include <boost/interprocess/managed_shared_memory.hpp>
  2. #include <boost/interprocess/sync/named_mutex.hpp>
  3. #include <iostream>
  4.  
  5. int main()
  6. {
  7. boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
  8. int *i = managed_shm.find_or_construct<int>("Integer")();
  9. boost::interprocess::named_mutex named_mtx(boost::interprocess::open_or_create, "mtx");
  10. named_mtx.lock();
  11. ++(*i);
  12. std::cout << *i << std::endl;
  13. named_mtx.unlock();
  14. }

除了一个参数用来指定互斥对象是被创建或者打开之外,boost::interprocess::named_mutex 的构造函数还需要一个名称参数。 每个知道此名称的应用程序能够访问这同一个对象。 为了获得对位于共享内存中数据的访问,应用程序需要通过调用 lock() 函数获得互斥对象的拥有关系。 由于互斥对象在任意时刻只能被一个应用程序拥有,其他应用程序需要等待,直到互斥对象被第一个应用程序使用 lock() 函数释放。 一旦应用程序获得互斥对象的所有权,它可以获得互斥对象保护的资源的排他访问。 在上面例子中,资源是int类的变量被递增并写到标准输出流中。

如果应用程序被启动多次,每个实例都会打印出和前一个值比较递增1的值。 感谢互斥对象,访问共享内存和变量本身在多个应用程序之间是同步的。

接下来的应用程序使用了定义在 boost/interprocess/sync/interprocess_mutex.hpp 文件中的 boost::interprocess::interprocess_mutex 类的匿名对象。 为了可以被所有应用程序访问,它存储在共享内存中。

  1. #include <boost/interprocess/managed_shared_memory.hpp>
  2. #include <boost/interprocess/sync/interprocess_mutex.hpp>
  3. #include <iostream>
  4.  
  5. int main()
  6. {
  7. boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
  8. int *i = managed_shm.find_or_construct<int>("Integer")();
  9. boost::interprocess::interprocess_mutex *mtx = managed_shm.find_or_construct<boost::interprocess::interprocess_mutex>("mtx")();
  10. mtx->lock();
  11. ++(*i);
  12. std::cout << *i << std::endl;
  13. mtx->unlock();
  14. }

这个应用程序的行为确实和前一个有点像。 唯一的不同是这次互斥对象通过用 boost::interprocess::managed_shared_memory 类的 construct()find_or_construct() 函数被直接存储在共享内存中。

除了 lock() 函数,boost::interprocess::named_mutexboost::interprocess::interprocess_mutex 还提供了 try_lock()timed_lock() 函数。 它们的行为和Boost.Thread提供的互斥对象相对应。

在需要递归互斥对象的时候,Boost.Interprocess 提供 boost::interprocess::named_recursive_mutexboost::interprocess::interprocess_mutex 两个对象可供使用。

在互斥对象保证共享资源的排他访问的时候,条件变量控制了在什么时候,谁必须具有排他访问权。 一般来讲,Boost.Interprocess 和 Boost.Thread 提供的条件变量工作方式相同。 它们有非常相似的接口,使熟悉 Boost.Thread 的用户在使用 Boost.Interprocess 的这些条件变量时立刻有一种自在的感觉。

  1. #include <boost/interprocess/managed_shared_memory.hpp>
  2. #include <boost/interprocess/sync/named_mutex.hpp>
  3. #include <boost/interprocess/sync/named_condition.hpp>
  4. #include <boost/interprocess/sync/scoped_lock.hpp>
  5. #include <iostream>
  6.  
  7. int main()
  8. {
  9. boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
  10. int *i = managed_shm.find_or_construct<int>("Integer")(0);
  11. boost::interprocess::named_mutex named_mtx(boost::interprocess::open_or_create, "mtx");
  12. boost::interprocess::named_condition named_cnd(boost::interprocess::open_or_create, "cnd");
  13. boost::interprocess::scoped_lock<boost::interprocess::named_mutex> lock(named_mtx);
  14. while (*i < 10)
  15. {
  16. if (*i % 2 == 0)
  17. {
  18. ++(*i);
  19. named_cnd.notify_all();
  20. named_cnd.wait(lock);
  21. }
  22. else
  23. {
  24. std::cout << *i << std::endl;
  25. ++(*i);
  26. named_cnd.notify_all();
  27. named_cnd.wait(lock);
  28. }
  29. }
  30. named_cnd.notify_all();
  31. boost::interprocess::shared_memory_object::remove("shm");
  32. boost::interprocess::named_mutex::remove("mtx");
  33. boost::interprocess::named_condition::remove("cnd");
  34. }

例子中使用的条件变量的类型 boost::interprocess::named_condition,定义在 boost/interprocess/sync/named_condition.hpp 文件中。 由于它是命名变量,所以它不需要存储在共享内存。

应用程序使用 while 循环递增一个存储在共享内存中的 int 类型变量而变量是在每个循环内重复递增,而它只在每两个循环时写出到标准输出中:写出的只能是奇数。

每次,在变量递增1之后,条件变量 named_cndwait ()函数被调用。 一个称作锁,在此例中是变量 lock 被传递给此函数。 这个锁和 Boost.Thread 中的锁含义相同:基于RAII概念的在构造函数中获得互斥对象的所有权,并在析构函数中释放它。

while 之前创建的锁因而在整个应用程序执行期间拥有互斥对象的所有权。 可是,如果作为一个参数传递给 wait() 函数,它会被自动释放。

条件变量常常用来等待一个信号,此信号会指示等待现在到了。 同步是通过 wait()notify_all() 函数控制的。 如果一个应用程序调用 wait() 函数,一直到对应的条件变量的 notify_all() 函数被调用,相应的互斥对象的所有权才会被被释放。

如果启动此程序,它看上去什么也没做:而只是变量在 while 循环内从0递增到1,然后应用程序使用 wait() 等待信号。 为了提供这个信号,应用程序需要再启动第二个实例。

应用程序的第二个实例将会在进入 while 循环之前,尝试获得同一个互斥对象的所有权。 这肯定是成功的,由于应用程序的第一个实例通过调用 wait() 释放了互斥对象的所有权。 因为变量已经递增了一次,第二个实例现在会执行 if 表达式的 else 分支,这使得在递增1之前将当前值写到标准输出流。

现在,第二个实例也调用了 wait() 函数,可是,在调用之前,它调用了 notify_all() 函数,这对于两个实例正确协作是非常重要的顺序。 第一个实例被通知并再次尝试获得互斥对象的所有权,虽然现在它还被第二个实例所拥有。 由于第二个实例在调用 notify_all() 之后调用了 wait(),这自动释放了所有权,第一个实例此时能够获得所有权。

两个实例交替地递增共享内存中的变量。 仅有一个实例将变量值写到标准输出流。 只要变量值到达10,while 循环结束。 为了让其他实例不必永远等待信号, notify_all() 函数在循环之后又被调用了一次。 在终止之前,共享内存,互斥对象和条件变量都被销毁。

就像有两种互斥对象,即必须存储在共享内存中匿名类型和命名类型,也存在两种类型的条件变量。 现在用匿名条件变量重写上面的例子。

  1. #include <boost/interprocess/managed_shared_memory.hpp>
  2. #include <boost/interprocess/sync/interprocess_mutex.hpp>
  3. #include <boost/interprocess/sync/interprocess_condition.hpp>
  4. #include <boost/interprocess/sync/scoped_lock.hpp>
  5. #include <iostream>
  6.  
  7. int main()
  8. {
  9. try
  10. {
  11. boost::interprocess::managed_shared_memory managed_shm(boost::interprocess::open_or_create, "shm", 1024);
  12. int *i = managed_shm.find_or_construct<int>("Integer")(0);
  13. boost::interprocess::interprocess_mutex *mtx = managed_shm.find_or_construct<boost::interprocess::interprocess_mutex>("mtx")();
  14. boost::interprocess::interprocess_condition *cnd = managed_shm.find_or_construct<boost::interprocess::interprocess_condition>("cnd")();
  15. boost::interprocess::scoped_lock<boost::interprocess::interprocess_mutex> lock(*mtx);
  16. while (*i < 10)
  17. {
  18. if (*i % 2 == 0)
  19. {
  20. ++(*i);
  21. cnd->notify_all();
  22. cnd->wait(lock);
  23. }
  24. else
  25. {
  26. std::cout << *i << std::endl;
  27. ++(*i);
  28. cnd->notify_all();
  29. cnd->wait(lock);
  30. }
  31. }
  32. cnd->notify_all();
  33. }
  34. catch (...)
  35. {
  36. }
  37. boost::interprocess::shared_memory_object::remove("shm");
  38. }

这个应用程序的工作完全和前一个例子一样,为了递增变量10次,因而也需要启动两个实例。 两个例子之间的差别很小。 与是否使用匿名或命名条件变量根本没有什么关系。

处理互斥对象和条件变量,Boost.Interprocess 还提供了叫做信号量和文件锁。 信号量的行为和条件变量相似,除了它不能区别两种状态,但它确是基于计数器的。 文件锁有些像互斥对象,虽然它们不是关于内存的对象,但它们确是文件系统上关于文件的对象。

就像 Boost.Thread 能够区分不同的互斥对象和锁,Boost.Interprocess 也提供了几个互斥对象和锁。 例如,互斥对象不仅能被排他地拥有,也可以不排他地所有。 这在多个应用程序需要同时读而排他写的时候非常有用。 对于不同的互斥对象,可以使用不同的具有RAII概念的锁类。

请注意如果不使用匿名同步对象,那么名称应该是唯一的。 虽然互斥对象和条件变量是基于不同类的对象,但也不必总是认为操作系统独立的接口是由 Boost.Interprocess 区别对待的。 在Windows系统上,互斥对象和条件变量使用同样的系统函数。 如果这两种对象使用相同的名称,那么应用程序在Windows上将不会正确地址执行。