桃源谷

心灵的旅行

人生就是一场旅行,不在乎旅行的目的地,在乎的是沿途的风景和看风景的心情 !
posts - 32, comments - 42, trackbacks - 0, articles - 0
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

用C++编写synchronized method比较难

Posted on 2010-07-17 12:17 lymons 阅读(3028) 评论(0)  编辑 收藏 引用 所属分类: C++CUnix/Linux文章翻译

在C++下编写synchronized method比较难 (1)Add star

在Java中有叫做synchronized这样一个方便的关键字。使用这个关键字的话,就可以像下面那样能够简单的进行"同步"method. 然而,被同步的method并不表示它就能在多线程中同时被执行.

public class Foo {    
     public synchronized boolean getFoo() { 
          
     }

那么、在C++ (with pthread)中如何能实现同样的功能呢? 首先,有一个最简单的方法就是下面这个.

// 方法 a
void Foo::need_to_sync(void{  
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  
pthread_mutex_lock(
&mutex);  
// 临界区处理  
pthread_mutex_unlock(&mutex);  
return;
}


这个方法, 暂且不说C语言, 就是在C++中下面的若干问题

  • 在临界区中间被return出来
  • 在临界区中间发生异常exception

发生的场合, mutex没有被解锁unlock。我们可以像下面代码那样对这点进行改进.

// 方法 b
class ScopedLock : private boost::noncopyable {
public:  explicit ScopedLock(pthread_mutex_t& m) : m_(m) {
    pthread_mutex_lock(
&m_);
  }
  
~ScopedLock(pthread_mutex_t& m) {
    pthread_mutex_unlock(
&m_);
  }
private:
  pthread_mutex_t
& m_;
};

void Foo::need_to_sync(void) {
  
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  {
 
// 虽然不加这个括号程序也没有问题。
    ScopedLock lock(mutex);

    
// 在此处添加处理
  }
  
return;
}

OK。return和异常的问题就可以解决了. 但是, 上面并没有完全解决这个问题,仍然有下面这个问题.

  1. 使用这个pthread_mutex_t并不是C++的.特别是存在下面的问题:
    • 不能和其他的Mutex类型做同样的处理
    • 与其他的Mutex类型使用同一个ScopedLock类,则不能lock
  2. Java的synchronized方法虽然可以"递归lock", 但是上面的代码并不是这样. 在临界区中递归调用自己的话就会发生死锁.

特别是,第2点的递归lock的问题是很重要的. 这里好好地使用glibc扩展的话就可以象下面那样解决.

/ 方法 c

void Foo::need_to_sync(void) {
  
static pthread_mutex_t mutex = PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP;

从NP*1这个后缀名就知道, 这个方法没有可移植性. 必须使用pthread_mutex_init来初始化递归mutex,而pthread_mutex_init函数在一个线程中只能被调用一次. 如果想要用synchronized method的方法来实现这个的话,就变成了"是先有鸡还是先有蛋"的话题了. 所以,用叫做pthread_once的函数来实现它,这也是在SUSv3中被记载的定则

// 方法 d

namespace /* anonymous */ {
  pthread_once_t      once 
= PTHREAD_ONCE_INIT;
  pthread_mutex_t     mutex;
  pthread_mutexattr_t attr;

  
void mutex_init() {
    pthread_mutexattr_init(
&attr);
    pthread_mutexattr_settype(
&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(
&mutex, &attr);
  }
}

void Foo::need_to_sync(void) {
  pthread_once(
&once, mutex_init);


  {
    ScopedLock 
lock(mutex);

    
// 処理

  }
  
return;
}

上面的代码就OK了。


这就能够解决递归lock的问题了.但是..., 这个方法

  • 这越来越不像C++的代码了.。对每一个想要同步(synchronize)的方法都像这么样写代码的话,效率变得非常低下.
  • 随机成本大。速度慢。

就会产生上面那样的新问题.


[] 在C++下编写synchronized method比较难(2)Add star

"不, 方法的同步应该是经常必需的, 并不是没有方便的办法",这样的说法也有吧. 是的, 有. 一般的办法是下面那样,

  • 做成一个Mutex,作为(non-POD型的, 即普通的)C++类
  • 做成一个Mutex类的实例,作为类变量, 或者是全局变量, 来同步化方法

来看看它的具体实现吧. 首先做成的Mutex类是下面那样*2

class Mutex {
public:
  Mutex() {
    pthread_mutex_init(
&m_, 0);
  }
  
void Lock(void) {
    pthread_mutex_lock(
&m_);
  }
  
void Unlock(void) {
    pthread_mutex_unlock(
&m_);
  }
private:
  pthread_mutex_t m_;
};

现在的Mutex类,被作为抽象基类(接口类)的场合也比较多. 在这里就不说了. ScopedLock类也需要有若干的修改. 想下面那样写就好.

template<typename T> class ScopedLock {
public:
  ScopedLock(T
& m) : _m(m) {
    _m.Lock();
  }
  
~ScopedLock() {
    _m.Unlock();
  }
private:
  T
& _m;
};

用这个Mutex类来同步方法, 就可以像下面那样写. 首先是看看一个明显的有错误的例子.

// 方法e

void Foo::need_to_sync(void) {
  
static Mutex mutex;
  {
    ScopedLock
<Mutex> lock(mutex);

    
// 処理

  }
  
return;
}

这是... 代码虽然简单易懂,但是很遗憾,它不能很好工作. NG!. Foo::need_to_sync函数第一次被执行的时候如果恰好是多个线程同时执行的话, mutex 的构造函数就有被多次调用的可能性.关于理由,可以参考微软中比较有名气的blog文章The Old New Thing、"C++ scoped static initialization is not thread-safe, on purpose!"在这里面有详尽的描述,所以就我们就不在详细叙述了*3。在这篇blog里使用了VC++的代码作为例子,但是g++也是差不多的。所以“动态的初始化局部的静态变量”是, 在线程所完全意识不到的情况下进行的*4


接下来,要介绍一个在目前做的比较好的方法。 为了简单我们使用了全局变量,但是即使作为类变量(类中的static成员变量)也是一样的。这个方法就是使用“非局部的静态变量”来做成Mutex。

// 方法f

namespace /* anonymous */ {
  Mutex mutex;
}

void Foo::need_to_sync(void) {
  ScopedLock
<Mutex> lock(mutex);

  
// 处理

  
return;
}

这个是最流行的方法,而且基本上可以没有问题就能工作得很好。

在一个全局的类对象x存在,且在x的构造函数中直接或者绕弯间接的调用Foo::need_to_sync函数的场合,会引起一些问题。也就是静态的对象的初始化顺序的问题,这个问题一般也被叫做"static initialization order fiasco" 。在执行到mutex的构造函数之前, mutex.Lock()有可能会被执行。


这里的FAQ的10.12~10.16*5、在里面对自己的代码的初始化顺序已经证明了没有问题,而且将来也不会出现问题,所以上面的方法是OK的。


如果, 初始化顺序的问题不能保证他没有问题的话, 只好使用pthread_once的“方法d”,或者移植性低的“方法c”。我的个人感觉是方法c还是比较不错的选择。


在最后我们尝试考虑一下如何把方法c变成C++的代码。

// 方法c (重新讨论)

void Foo::need_to_sync(void) {
static pthread_mutex_t mutex = PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP;

目标是、

  • 隐藏pthread_mutex_t类型、让自己写的类的类型可见。
  • 在方法e,f中像使用ScopedLock模板那样进行修改。

当然,不让它发生初始化顺序的问题。


[] 用C++编写synchronized method比较难 (3)Add star

这是方法c的改良。 首先, 为了避免发生初始化顺序的问题, 必须是不允许调用构造函数就能完成对象的初始化。因此,必须像下面那样初始化mutex对象

// 方法c' (假设)

void Foo::need_to_sync(void) {
static StaticMutex mutex = { PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP, ........ };

一般的不允许像这样初始化C++类。为了实现上面那样的初始化,StaticMutex类必须是POD型的。所谓POD型就是,

  • 不允许有构造函数
  • 不允许有析构函数
  • 不允许编译器生成拷贝构造函数, 赋值构造函数。
  • 不允许有private, protected 的成员
  • 不允许有虚函数

满足以上规格的类型*6


大概有严格的制约,但是利用"定义非虚成员函数是没有问题的"这个特性, 我们尝试改良方法c的方案.


...像下面那样如何?

// 方法c'

#define POD_MUTEX_MAGIC 0xdeadbeef
#define STATIC_MUTEX_INITIALIZER           { PTHREAD_INITIALIZER,              POD_MUTEX_MAGIC }
#define STATIC_RECURSIVE_MUTEX_INITIALIZER { PTHREAD_RECURSIVE_INITIALIZER_NP, POD_MUTEX_MAGIC }

class PODMutex {
public:
  
void Lock() {
    assert(magic_ 
== POD_MUTEX_MAGIC);
    pthread_mutex_lock(
&mutex_);
  }
  
void Unlock() {
    assert(magic_ 
== POD_MUTEX_MAGIC);
    pthread_mutex_unlock(
&mutex_);
  }
  typedef ScopedLock
<PODMutex> ScopedLock;

public:
  
// 虽然编程了POD型, 但是不定义成public就是无效的
  pthread_mutex_t mutex_;
  
const unsigned int magic_;
};

// ScopedLock类模板是留用了在方法e,f中做成的代码.

void Foo::need_to_sync(void) {
  
static PODMutex mutex = STATIC_RECURSIVE_MUTEX_INITIALIZER;
  {
    PODMutex::ScopedLock 
lock(mutex);

    
// 处理.

  }
  
return;
}

上面的代码满足了"隐藏了pthread_mutex_t型,留用了ScopedLock<>"这两个目的. 这不就是有点儿像C++的代码了吗? 还有,PODMutex类型是即使在上记例子中那样的局部静态变量以外,也能放心的使用全局变量,类变量了.

而且, 成员变量 magic_ 是, 一个const成员变量, 所以当使用编译器自动生成的构造函数来创建一个对象时就会发生错误. 因此,在构建release版程序时把它剔除就好了.


使用g++ -S来编译上面的代码, 生成汇编代码. 我们就能看见下面那样的局部的静态变量.

$ g++ -S sample.cpp
$ c++filt < sample.s | lv
(略)
.size Foo::need_to_sync()::mutex, 28
Foo::need_to_sync()::mutex:
.long 0
.long 0
.long 0
.long 1
.long 0
.long 0
.long -559038737

0,0,0,1,0,0 这样的东西是 PTHREAD_RECURSIVE_INITIALIZER_NP , -559038737 则是 POD_MUTEX_MAGIC 。即使没有进行动态的初始化(不调用构造函数)、仅仅是在目标文件上生成的目标代码那样的进行静态初始化, mutex对象也能被正常的初始化, 所以这段代码是OK的.


随便, 在使用boost库的场合, 方法f之外的选择余地几乎没有(至少是现在). 一看见ML等, (当然!!)就知道可能会出现 order顺序的问题. 但是, 就目前来讲, 既要保证既要保证可移植性*7和速度,又要能做成与方法c相当的PODMutex的方法好像还没有出现吧.


完结

*1:non portable 的意思

*2:递归mutex的例子的代码太冗长了,这里就省略 了. 根据pthread_mutex_init来进行初始化,就使得做成递归mutex变得比较容易了.

*3:这里记载了 g++ -S的结果和解说

*4:2005/12追记: 在最近的g++中发生异常、参照这里 http://d.hatena.ne.jp/yupo5656/20051215/p2

*5:日语文献 ASIN:489471194X 中记载着翻译版. 还有 static initialization order 的问题,在此处也有一些记载.

*6:详细是参看 ISO/IEC 14882:2003 或者是 JIS X 3014:2003 的「§3.9/10 C互換型」「§8.5.1/14 静的記憶期間をもつC互換型の集成体の波括弧で囲んだ初期化子並びによる静的な初期化」「§9/4 C互換構造体」这几个章节

*7:Windows是如何做的? 可能不能解决这个命题 - 推测


http://d.hatena.ne.jp/yupo5656/20051215/p2

http://d.hatena.ne.jp/yupo5656/20041011#p1

http://blogs.msdn.com/b/oldnewthing/archive/2004/03/08/85901.aspx

http://d.hatena.ne.jp/yupo5656/20071008/p1


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


我的个人简历第一页 我的个人简历第二页