話說Java裡有個很強大的關鍵字叫synchronized,可以方便的實現線程同步。今天異想天開,嘗試在C++裡類比一個類似的。
最近在學習C++的STL,看見智能指標這章節時,無不感歎利用語言的豐富特徵,來各種實現各種巧妙的構思。最經典的莫過於使用棧物件建構/解構函式,來維護局部資源的初始化和釋放。照著這個巧妙的方法,依樣畫葫蘆自己也來寫一個,來實現局部代碼線程同步。
Java裡的synchronized有兩種形式,一種是基於函數的,另種則是語塊的。前者受C++的文法所限,估計是沒法實現了,所以就嘗試後者。
塊級文法很簡單:
synchronized(syncObject) { // code}
因為Java所有變數都繼承於Object,所以任意變數都能當作鎖用。這在C++裡無法簡易實現,因此我們用特定的類型執行個體當作同步變數使用。
先從最經典簡易的同步類說起。
struct Lock : CRITICAL_SECTION { Lock() { ::InitializeCriticalSection(this); } ~Lock() { ::DeleteCriticalSection(this); } void Enter() { ::EnterCriticalSection(this); } void Leave() { ::LeaveCriticalSection(this); }};
這是windows下實現線程同步最常見的封裝。只需聲明一個Lock執行個體,在需要同步的代碼前後分別調用Enter和Leave即可。
既然用起來這麼簡單,為什麼還要繼續改進?顯然這種方法有個很大的缺陷,如果忘了調用Leave,或者在調用之前就return/throw退出,那麼就會引起死結。
所以,我們需要類似auto_ptr的機制,自動維護棧資料的建立和刪除。就暫且稱它_auto_lock吧。
struct _auto_lock { Lock& _lock; _auto_lock(Lock& lock) : _lock(lock) { _lock.Enter(); } ~_auto_lock() { _lock.Leave(); }};
_auto_lock通過引用一個Lock執行個體來初始化,並立即鎖住臨界區;被銷毀時則釋放鎖。
有了這個機制,我們再也不用擔心忘了調用.Leave()。只需提供一個Lock對象,就能在當前語塊自動加鎖解鎖。再也不用擔心死結的問題了。
Lock mylock; void Test(){ // code1 ... // syn code { _auto_lock x(mylock); } // code2 ...}
進入syn code的"{"之後,_auto_lock被構造;無論用那種方式離開"}",解構函式都會被調用。
上述代碼類似的在stl和boost裡都是及其常見的。利用棧對象的構造/解構函式維護局部資源,算是C++很常用的一技巧。
我們的目標又近了一步。下面開始利用經典的宏定義,製造一顆synchronized文法糖,最終實現這樣的文法:
Lock mylock; void Test(){ // code1 ... synchronized(mylock) { // sync code } // code2 ...}
顯然需要一個叫synchronized宏,並且在裡面定義_auto_lock。
#define synchronized(lock) ..... _auto_lock x(lock) ......
乍一看這文法很像迴圈,並且要在迴圈內定義變數,所以用for(;;)的結構是再好不過了。
for(_auto_lock x(mylock); ; )
不過sync code我們只需執行一次,所以還需另一個變數來控制次數。由於for裡面只能聲明一種類型的變數,所以我們在外面再套一層迴圈:
for(int _i=0; _i<1; _i++)for(_auto_lock x(mylock); _i<1; _i++)
synchronized宏將mylock替換成上述代碼,既沒有違反文法,也實現相同的流程。得益於迴圈文法,甚至可以在synchronized內使用break來跳出同步塊!
我們將上述代碼整理下,並做個簡單的測試。
#include <stdio.h>#include <windows.h>#include <process.h>struct Lock : CRITICAL_SECTION { Lock() { ::InitializeCriticalSection(this); } ~Lock() { ::DeleteCriticalSection(this); } void Enter() { ::EnterCriticalSection(this); } void Leave() { ::LeaveCriticalSection(this); }};struct _auto_lock { Lock& _lock; _auto_lock(Lock& lock) : _lock(lock) { _lock.Enter(); } ~_auto_lock() { _lock.Leave(); }};#define synchronized(lock) for(int _i=0; _i<1; _i++)for(_auto_lock lock##_x(lock); _i<1; _i++)// ---------- demo ----------Lock mylock;// ---------- test1 ----------void WaitTest(int id){ printf("No.%d waiting...\n", id); synchronized(mylock) { Sleep(1000); } printf("No.%d done\n", id);}void Test1(){ _beginthread((void(__cdecl*)(void*))WaitTest, 0, (void*) 1); _beginthread((void(__cdecl*)(void*))WaitTest, 0, (void*) 2); _beginthread((void(__cdecl*)(void*))WaitTest, 0, (void*) 3);}// ---------- test2 ----------void ThrowFunc(int id){ printf("No.%d waiting...\n", id); synchronized(mylock) { Sleep(1000); throw "some err"; } printf("No.%d done\n", id);}void ThrowTest(int id){ try { ThrowFunc(id); } catch(...) { printf("%d excepted\n", id); }}void Test2(){ _beginthread((void(__cdecl*)(void*))ThrowTest, 0, (void*) 1); _beginthread((void(__cdecl*)(void*))ThrowTest, 0, (void*) 2); _beginthread((void(__cdecl*)(void*))ThrowTest, 0, (void*) 3);}// ---------- test3 ----------void BreakTest(int id){ printf("No.%d waiting...\n", id); synchronized(mylock) { Sleep(1000); break; Sleep(99999999); } printf("No.%d done\n", id);}void Test3(){ _beginthread((void(__cdecl*)(void*))BreakTest, 0, (void*) 1); _beginthread((void(__cdecl*)(void*))BreakTest, 0, (void*) 2); _beginthread((void(__cdecl*)(void*))BreakTest, 0, (void*) 3);}int main(int argc, char* argv[]){ printf("Wait Test. Press any key to start...\n"); getchar(); Test1(); getchar(); printf("Exception Test. Press any key to start...\n"); getchar(); Test2(); getchar(); printf("Break Test. Press any key to start...\n"); getchar(); Test3(); getchar(); return 0;}
使用文法糖除了好看外,有個最重要的功能就是可以在synchronized同步塊裡使用break來跳出,並且不會引起死結,這是其他方法無法實現的。