話說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來跳出,並且不會引起死鎖,這是其他方法無法實現的。