在C++下編寫synchronized method比較難 (1)
在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和異常的問題就可以解決了. 但是, 上面并沒有完全解決這個問題,仍然有下面這個問題.
- 使用這個pthread_mutex_t并不是C++的.特別是存在下面的問題:
- 不能和其他的Mutex類型做同樣的處理
- 與其他的Mutex類型使用同一個ScopedLock類,則不能lock
- Java的synchronized方法雖然可以"遞歸lock", 但是上面的代碼并不是這樣. 在臨界區中遞歸調用自己的話就會發生死鎖.
特別是,第2點的遞歸lock的問題是很重要的. 這里好好地使用glibc擴展的話就可以象下面那樣解決.
/ 方法 c
void Foo::need_to_sync(void) {
static pthread_mutex_t mutex = PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP;
從NPpthread_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++] 在C++下編寫synchronized method比較難(2)
"不, 方法的同步應該是經常必需的, 并不是沒有方便的辦法",這樣的說法也有吧. 是的, 有. 一般的辦法是下面那樣,
- 做成一個Mutex,作為(non-POD型的, 即普通的)C++類
- 做成一個Mutex類的實例,作為類變量, 或者是全局變量, 來同步化方法
來看看它的具體實現吧. 首先做成的Mutex類是下面那樣。
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、在這里面有詳盡的描述,所以就我們就不在詳細敘述了。在這篇blog里使用了VC++的代碼作為例子,但是g++也是差不多的。所以“動態的初始化局部的靜態變量”是, 在線程所完全意識不到的情況下進行的。
接下來,要介紹一個在目前做的比較好的方法。 為了簡單我們使用了全局變量,但是即使作為類變量(類中的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、在里面對自己的代碼的初始化順序已經證明了沒有問題,而且將來也不會出現問題,所以上面的方法是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++] 用C++編寫synchronized method比較難 (3)
這是方法c的改良。 首先, 為了避免發生初始化順序的問題, 必須是不允許調用構造函數就能完成對象的初始化。因此,必須像下面那樣初始化mutex對象
// 方法c' (假設)
void Foo::need_to_sync(void) {
static StaticMutex mutex = { PTHREAD_MUTEX_RECURSIVE_INITIALIZER_NP, ........ };
一般的不允許像這樣初始化C++類。為了實現上面那樣的初始化,StaticMutex類必須是POD型的。所謂POD型就是,
- 不允許有構造函數
- 不允許有析構函數
- 不允許編譯器生成拷貝構造函數, 賦值構造函數。
- 不允許有private, protected 的成員
- 不允許有虛函數
滿足以上規格的類型。
大概有嚴格的制約,但是利用"定義非虛成員函數是沒有問題的"這個特性, 我們嘗試改良方法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的方法好像還沒有出現吧.
完結