# Windows Critical Section API ### Mutex鎖的概念 假設有以下運算要進行: 0 + 3 + 5 - 2 + 7 val = 0; 如果是Single Thread下,可以照順序寫下所有運算 val = val + 3; val = val + 5; val = val - 2; val = val + 7; 如果要讓多個thread各自去執行一部分運算,在沒有考慮thread-safe的情形下,有可能發生以下錯誤: ** Thread的執行順序是不確定的 // Thread A負責+3 // 假設取到的val是0 val = val + 3; result: 3 // Thread B負責+5 // 假設這時thread A還未將結果存到val,因此取到的值還是0 val = 0 + 5; result: 5 // Thread C負責-2 val = val - 2; // 假設取到的val是Thread A運算完的3 result: 1 // Thread D負責+7 // 假設取到的val是thread C運算完的1 val = val + 7; result: 8 可以看到結果完全不正確;要避免這個結果,只要確保每一道運算在同一時間裡不同時進行即可,因此我們會寫一個簡單function來處理單一運算,並且將這個function用critical section保護起來 (這裡有一個限制是threads必須拿global變數val做運算,而不是用threads自己持有的local變數,最後再由主程式去統整結果。雖然後者就不會有thread-safe的問題,但這樣就失去multi-thread的意義) ### Windows Critical Section API 每個支援multi-thread的作業系統應該提供相應的資料保護機制,以避免資源競爭的問題,Windows則是提供一組Critical Section API: InitializeCriticalSection EnterCriticalSection LeaveCriticalSection DeleteCriticalSection 實際使用方式如下: ```cpp #include <windows.h> CRITICAL_SECTION g_mtx; int g_val = 0; int g_operand[] = {3, 5, -2, 7}; int main() { InitializeCriticalSection(g_mtx); // start threads DWORD threadID[4]; HANDLE threadHandle[4]; for (int i = 0; i < 4; ++i) { threadHandle[i] = CreateThread(0, 0, thread_proc, i, 0, &threadID[i]); } DeleteCriticalSection(g_mtx); return 0; } DWORD WINAPI thread_proc(LPVOID lpParameter) { int index = (int)lpParameter; add(g_operand[index]); } void add(int Operant) { EnterCriticalSection(g_mtx); g_val = g_val + Operant; LeaveCriticalSection(g_mtx); } ``` 因為單純的加減法運算符合結合律交換律,無論四個thread順序如何排列最終結果都會正確 這邊CRITICAL_SECTION是一個鎖的概念,如果一個function會被多個thread執行,每個thread 在進入critical section時必須傳遞同一個鎖,如果傳遞不同一個鎖,則無法達到保護目的 ### 設計模式 (Design Pattern) 設計模式是將一系列行為架構用物件導向的方式封裝起來,使得程式在重複該行為時可以做得更簡潔,以下是兩種Critical Section的設計模式 (ObjectLevelLockable和ClassLevelLockable): ```cpp #ifndef ThreadsH #define ThreadsH //----------------------------------------------------------------------------- #include "Pch.h" //----------------------------------------------------------------------------- // 這個class只是簡單地將Critical Section API包裝成一個class,沒什麼特別 class Mutex { public: Mutex() { ::InitializeCriticalSection(&_mtx); } ~Mutex() { ::DeleteCriticalSection(&_mtx); } void Lock() { ::EnterCriticalSection(&_mtx); } void Unlock() { ::LeaveCriticalSection(&_mtx); } private: mutable CRITICAL_SECTION _mtx; }; // 這個class利用了destructor,使得LeaveCriticalSection在其使用函式結束時會被自動呼叫 template < class Host, class MutexPolicy = Mutex > class ObjectLevelLockable { private : mutable MutexPolicy _mtx; public: ObjectLevelLockable() : _mtx() {} ObjectLevelLockable(const ObjectLevelLockable&) : _mtx() {} ~ObjectLevelLockable() {} class Lock; friend class Lock; class Lock { public: explicit Lock(const ObjectLevelLockable& host) : _host(host) { _host._mtx.Lock(); } explicit Lock(const ObjectLevelLockable* host) : _host(*host) { _host._mtx.Lock(); } ~Lock() { _host._mtx.Unlock(); } private: Lock(); Lock(const Lock&); Lock& operator=(const Lock&); const ObjectLevelLockable& _host; }; }; //----------------------------------------------------------------------------- // 這個class將整個Mutex鎖變成全域的,只要是繼承它的子class // 無論該class產生了幾個object實例,在呼叫function時都會參考到同一個鎖 template <class Host, class MutexPolicy = Mutex > class ClassLevelLockable { private : class Initializer { public : Initializer() : _init(true), _mtx() { } ~Initializer() { } public : bool _init; MutexPolicy _mtx; }; static Initializer _initializer; public: class Lock; friend class Lock; class Lock { public: Lock() { _initializer._mtx.Lock(); } explicit Lock(const ClassLevelLockable&) { _initializer._mtx.Lock(); } explicit Lock(const ClassLevelLockable*) { _initializer._mtx.Lock(); } ~Lock() { _initializer._mtx.Unlock(); } private: Lock(const Lock&); Lock& operator=(const Lock&); }; }; template < class Host, class MutexPolicy > typename ClassLevelLockable< Host, MutexPolicy >::Initializer ClassLevelLockable< Host, MutexPolicy >::_initializer; //----------------------------------------------------------------------------- #endif ``` ### 說明 ObjectLevelLockable和ClassLevelLockable目標是打造為繼承的class提供multu-thread保護 假設以下場景,有一個ClassA全域變數,裡面儲存了一項數值,並且有用來加減該數值的function,該數值需要被保護以確保不會被同時存取 ```cpp static ClassA g_ca; // Thread裡面會呼叫到存取單一資源的function,因此該function在multi-thread裡不能被同時呼叫 DWORD WINAPI thread_proc(LPVOID lpParameter) { // 隨機產生加減值 int value = rand(); bool sign = ((rand() % 100 + 1) - (rand() % 100 + 1)) > 50 ? true : false; if (sign) value = -value; g_ca.add(value); } ``` 如果單純只使用Critical Section API,ClassA設計如下: ```cpp class ClassA { public: // Constructor ClassA() { InitializeCriticalSection(_mtx); } ~ClassA() { DeleteCriticalSection(_mtx); } void add(int value) { EnterCriticalSection(_mtx); // The code to protect resource += value LeaveCriticalSection(_mtx); } // 讀取最新的resource值 int get_resource_val() const { return _resource; } private: mutable CRITICAL_SECTION _mtx; int _resource; }; ``` 如果使ClassA繼承ObjectLevelLockable,程式會比較簡潔 ```cpp class ClassA : public ObjectLevelLockable<ClassA> { public: // Constructor ClassA() { } ~ClassA() { } void add(int value) { Lock lock(*this); // The code to protect _resource += value } // 讀取最新的resource值 int get_resource_val() const { return _resource; } private: int _resource; }; ``` 如果是一個class同時可能有多個object,這些object都可能存取到同一項資源,則需要用ClassLevelLockable 須注意這裡的_resource宣告使用了static,也就是它是一個全域變數,它的生命週期是整個程式開始時就存在,而不是隨著ClassA object的產生才存在 (上一個範例中,只有一個全域的ClassA object,因此_resource不需要宣告為static;下個範例中ClassA可能有很多個,用來操作同一個變數_resource,因此_resource必須是全域) ```cpp class ClassA : public ClassLevelLockable<ClassA> { public: // Constructor ClassA() { } ~ClassA() { } void add(int value) { Lock lock(*this); // The code to protect _resource += value } // 讀取最新的resource值 int get_resource_val() const { return _resource; } private: static int _resource; }; ``` ```cpp DWORD WINAPI thread_proc(LPVOID lpParameter) { // ClassA不再是全域變數,而是隨著thread產生的區域變數,因此可能同時有很多個 ClassA ca; int value = rand(); bool sign = ((rand() % 100 + 1) - (rand() % 100 + 1)) > 50 ? true : false; if (sign) value = -value; ca.add(value); } ``` 在使用ClassLevelLockable之後,只要宣告了ClassLevelLockable<ClassA>,那麼整個程式的生命週期中,就存在一個用來保護ClassA中需要保護的function的鎖,無論有幾個ClassA object實例,都會使用同一個鎖來保護資料