上個星期去綠盟科技面試C++開發實習生,期中被問到互斥量與臨界區的區別,當時睜著我水汪汪的大眼睛不知如何是好。我沒有準備好,孫鑫的MFC視訊也沒看通透。所以答不出來。畢竟是去練一下膽量和走一下流程。現篩選一些有用的資訊整合起來,以防以後用到。嘿嘿,我無恥地將其設為自己的原創。
不管是輔助執行緒還是使用者介面執行緒,在存取共享資源時,都需要保護共享資源,以免引起衝突,造成錯誤。處理方法類似於Win32 API函式的使用,但MFC為我們提供了幾個同步物件C++類,即CSyncObject、CMutex、CSemaphore、CEvent、CCriticalSection。這裡,CSyncObject為其它四個類的基類,後四個類分別對應前面所講的四個Win32 API同步物件。
通常,我們在C++物件的成員函式中使用共享資源,或者把共享資源封裝在C++類的內部。我們可將執行緒同步操作封裝在物件類的實現函式當中,這樣在應用中的執行緒使用C++物件時,就可以像一般物件一樣使用它,簡化了使用部分程式碼的編寫,這正是物件導向程式設計的思想。這樣編寫的類被稱作”執行緒安全類”。在設計執行緒安全類時,首先應根據具體情況在類中加入一個同步物件類資料成員。然後,在類的成員函式中,凡是所有修改公共資料或者讀取公共資料的地方均要加入相應的同步呼叫。一般的處理步驟是:建立一個CSingleLock或者CMultiLock物件,然後呼叫其Lock函式。當物件結束時,自動在解構函式中呼叫Unlock函式,當然也可以在任何希望的地方呼叫Unlock函式。
如果不是在特定的C++物件中使用共享資源,而是在特定的函式中使用共享資源(這樣的函式稱為”執行緒安全函式”),那麼還是按照前面介紹的辦法去做:先建立同步物件,然後呼叫等待函式,直到可以訪問資源,最後釋放對同步物件的控制。
下面我們討論四個同步物件分別適用的場合:
(1)如果某個執行緒必須等待某些事件發生後才能存取相應資源,則用CEvent;
(2)如果一個應用同時可以有多個執行緒存取相應資源,則用CSemaphore;
(3)如果有多個應用(多個程式)同時存取相應資源,則用CMutex,否則用CCriticalSection。
使用執行緒安全類或者執行緒安全函式進行程式設計,比不考慮執行緒安全的程式設計要複雜,尤其在進行除錯時情況更為複雜,我們必須靈活使用Visual C++提供的除錯工具,以保證共享資源的安全存取。執行緒安全程式設計的另一缺點是執行效率相對要低些,即使在單個執行緒執行的情況下也會損失一些效率。所以,我們在實際工作中應具體問題具體分析,以選擇合適的程式設計方法。
OS中的解說及詳細程式設計使用:
1.臨界區:通過對多執行緒的序列化來訪問公共資源或一段程式碼,速度快,適合控制資料訪問。
2.互斥量:為協調共同對一個共享資源的單獨訪問而設計的。
3.訊號量:為控制一個具有有限數量使用者資源而設計。
4.事 件:用來通知執行緒有一些事件已發生,從而啟動後繼任務的開始。
臨界區(Critical Section)
保證在某一時刻只有一個執行緒能訪問資料的簡便辦法。在任意時刻只允許一個執行緒對共享資源進行訪問。如果有多個執行緒試圖同時訪問臨界區,那麼在有一個執行緒進入後其他所有試圖訪問此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操作共享資源的目的。
臨界區包含兩個操作原語:EnterCriticalSection() 進入臨界區LeaveCriticalSection() 離開臨界區
EnterCriticalSection()語句執行後程式碼將進入臨界區以後無論發生什麼,必須確保與之匹配的LeaveCriticalSection()都能夠被執行到。否則臨界區保護的共享資源將永遠不會被釋放。雖然臨界區同步速度很快,但卻只能用來同步本程式內的執行緒,而不可用來同步多個程式中的執行緒。
MFC提供了很多功能完備的類,我用MFC實現了臨界區。MFC為臨界區提供有一個CCriticalSection類,使用該類進行執行緒同步處理是非常簡單的。只需線上程函式中用CCriticalSection類成員函式Lock()和UnLock()標定出被保護程式碼片段即可。Lock()後程式碼用到的資源自動被視為臨界區內的資源被保護。UnLock後別的執行緒才能訪問這些資源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
//CriticalSection CCriticalSection global_CriticalSection; // 共享資源 char global_Array[256]; //初始化共享資源 void InitializeArray() { for(int i = 0;i<256;i++) { global_Array[i]=I; } } //寫執行緒 UINT Global_ThreadWrite(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); //進入臨界區 global_CriticalSection.Lock(); for(int i = 0;i<256;i++) { global_Array[i]=W; ptr->SetWindowText(global_Array); Sleep(10); } //離開臨界區 global_CriticalSection.Unlock(); return 0; } //刪除執行緒 UINT Global_ThreadDelete(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); //進入臨界區 global_CriticalSection.Lock(); for(int i = 0;i<256;i++) { global_Array[i]=D; ptr->SetWindowText(global_Array); Sleep(10); } //離開臨界區 global_CriticalSection.Unlock(); return 0; } //建立執行緒並啟動執行緒 void CCriticalSectionsDlg::OnBnClickedButtonLock() { //Start the first Thread CWinThread *ptrWrite = AfxBeginThread(Global_ThreadWrite, &m_Write, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); ptrWrite->ResumeThread(); //Start the second Thread CWinThread *ptrDelete = AfxBeginThread(Global_ThreadDelete, &m_Delete, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); ptrDelete->ResumeThread(); } |
在測試程式中,Lock UnLock兩個按鈕分別實現,在有臨界區保護共享資源的執行狀態,和沒有臨界區保護共享資源的執行狀態。
互斥量(Mutex)
互斥量跟臨界區很相似,只有擁有互斥物件的執行緒才具有訪問資源的許可權,由於互斥物件只有一個,因此就決定了任何情況下此共享資源都不會同時被多個執行緒所訪問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交出,以便其他執行緒在獲得後得以訪問資源。互斥量比臨界區複雜。因為使用互斥不僅僅能夠在同一應用程式不同執行緒中實現資源的安全共享,而且可以在不同應用程式的執行緒之間實現對資源的安全共享。
互斥量包含的幾個操作原語:
CreateMutex() 建立一個互斥量
OpenMutex() 開啟一個互斥量
ReleaseMutex() 釋放互斥量
WaitForMultipleObjects() 等待互斥量物件
同樣MFC為互斥量提供有一個CMutex類。使用CMutex類實現互斥量操作非常簡單,但是要特別注意對CMutex的建構函式的呼叫
CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)
不用的引數不能亂填,亂填會出現一些意想不到的執行結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
//建立互斥量 CMutex global_Mutex(0,0,0); // 共享資源 char global_Array[256]; void InitializeArray() { for(int i = 0;i<256;i++) { global_Array[i]=I; } } UINT Global_ThreadWrite(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); global_Mutex.Lock(); for(int i = 0;i<256;i++) { global_Array[i]=W; ptr->SetWindowText(global_Array); Sleep(10); } global_Mutex.Unlock(); return 0; } UINT Global_ThreadDelete(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); global_Mutex.Lock(); for(int i = 0;i<256;i++) { global_Array[i]=D; ptr->SetWindowText(global_Array); Sleep(10); } global_Mutex.Unlock(); return 0; } |
同樣在測試程式中,Lock UnLock兩個按鈕分別實現,在有互斥量保護共享資源的執行狀態,和沒有互斥量保護共享資源的執行狀態。
訊號量(Semaphores)
訊號量物件對執行緒的同步方式與前面幾種方法不同,訊號允許多個執行緒同時使用共享資源,這與作業系統中的PV操作相同。它指出了同時訪問共享資源的執行緒最大數目。它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目。在用CreateSemaphore()建立訊號量時即要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會減1,只要當前可用資源計數是大於0的,就可以發出訊號量訊號。但是當前可用計數減小到0時則說明當前佔用資源的執行緒數已經達到了所允許的最大數目,不能在允許其他執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離開的同時通過ReleaseSemaphore()函式將當前可用資源計數加1。在任何時候當前可用資源計數決不可能大於最大資源計數。
PV操作及訊號量的概念都是由荷蘭科學家E.W.Dijkstra提出的。訊號量S是一個整數,S大於等於零時代表可供併發程式使用的資源實體數,但S小於零時則表示正在等待使用共享資源的程式數。
P操作申請資源:
(1)S減1;
(2)若S減1後仍大於等於零,則程式繼續執行;
(3)若S減1後小於零,則該程式被阻塞後進入與該訊號相對應的佇列中,然後轉入程式排程。
V操作 釋放資源:
(1)S加1;
(2)若相加結果大於零,則程式繼續執行;
(3)若相加結果小於等於零,則從該訊號的等待佇列中喚醒一個等待程式,然後再返回原程式繼續執行或轉入程式排程。
訊號量包含的幾個操作原語:
CreateSemaphore() 建立一個訊號量
OpenSemaphore() 開啟一個訊號量
ReleaseSemaphore() 釋放訊號量
WaitForSingleObject() 等待訊號量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
//訊號量控制程式碼 HANDLE global_Semephore; // 共享資源 char global_Array[256]; void InitializeArray() { for(int i = 0;i<256;i++) { global_Array[i]=I; } } //執行緒1 UINT Global_ThreadOne(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); //等待對共享資源請求被通過 等於 P操作 WaitForSingleObject(global_Semephore, INFINITE); for(int i = 0;i<256;i++) { global_Array[i]=O; ptr->SetWindowText(global_Array); Sleep(10); } //釋放共享資源 等於 V操作 ReleaseSemaphore(global_Semephore, 1, NULL); return 0; } UINT Global_ThreadTwo(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); WaitForSingleObject(global_Semephore, INFINITE); for(int i = 0;i<256;i++) { global_Array[i]=T; ptr->SetWindowText(global_Array); Sleep(10); } ReleaseSemaphore(global_Semephore, 1, NULL); return 0; } UINT Global_ThreadThree(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); WaitForSingleObject(global_Semephore, INFINITE); for(int i = 0;i<256;i++) { global_Array[i]=H; ptr->SetWindowText(global_Array); Sleep(10); } ReleaseSemaphore(global_Semephore, 1, NULL); return 0; } void CSemaphoreDlg::OnBnClickedButtonOne() { //設定訊號量 1 個資源 1同時只可以有一個執行緒訪問 global_Semephore= CreateSemaphore(NULL, 1, 1, NULL); this->StartThread(); // TODO: Add your control notification handler code here } void CSemaphoreDlg::OnBnClickedButtonTwo() { //設定訊號量 2 個資源 2 同時只可以有兩個執行緒訪問 global_Semephore= CreateSemaphore(NULL, 2, 2, NULL); this->StartThread(); // TODO: Add your control notification handler code here } void CSemaphoreDlg::OnBnClickedButtonThree() { //設定訊號量 3 個資源 3 同時只可以有三個執行緒訪問 global_Semephore= CreateSemaphore(NULL, 3, 3, NULL); this->StartThread(); // TODO: Add your control notification handler code here } |
訊號量的使用特點使其更適用於對Socket(套接字)程式中執行緒的同步。例如,網路上的HTTP伺服器要對同一時間內訪問同一頁面的使用者數加以限制,這時可以為每一個使用者對伺服器的頁面請求設定一個執行緒,而頁面則是待保護的共享資源,通過使用訊號量對執行緒的同步作用可以確保在任一時刻無論有多少使用者對某一頁面進行訪問,只有不大於設定的最大使用者數目的執行緒能夠進行訪問,而其他的訪問企圖則被掛起,只有在有使用者退出對此頁面的訪問後才有可能進入。
事件(Event)
事件物件也可以通過通知操作的方式來保持執行緒的同步。並且可以實現不同程式中的執行緒同步操作。
訊號量包含的幾個操作原語:
CreateEvent() 建立一個訊號量
OpenEvent() 開啟一個事件
SetEvent() 回置事件
WaitForSingleObject() 等待一個事件
WaitForMultipleObjects()
等待多個事件
WaitForMultipleObjects 函式原型:
WaitForMultipleObjects(
IN DWORD nCount, // 等待控制程式碼數
IN CONST HANDLE *lpHandles, //指向控制程式碼陣列
IN BOOL bWaitAll, //是否完全等待標誌
IN DWORD dwMilliseconds //等待時間
)
引數nCount指定了要等待的核心物件的數目,存放這些核心物件的陣列由lpHandles來指向。fWaitAll對指定的這nCount個核心物件的兩種等待方式進行了指定,為TRUE時當所有物件都被通知時函式才會返回,為FALSE則只要其中任何一個得到通知就可以返回。dwMilliseconds在這裡的作用與在WaitForSingleObject()中的作用是完全一致的。如果等待超時,函式將返回WAIT_TIMEOUT。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
//事件陣列 HANDLE global_Events[2]; // 共享資源 char global_Array[256]; void InitializeArray() { for(int i = 0;i<256;i++) { global_Array[i]=I; } } UINT Global_ThreadOne(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); for(int i = 0;i<256;i++) { global_Array[i]=O; ptr->SetWindowText(global_Array); Sleep(10); } //回置事件 SetEvent(global_Events[0]); return 0; } UINT Global_ThreadTwo(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); for(int i = 0;i<256;i++) { global_Array[i]=T; ptr->SetWindowText(global_Array); Sleep(10); } //回置事件 SetEvent(global_Events[1]); return 0; } UINT Global_ThreadThree(LPVOID pParam) { CEdit *ptr=(CEdit *)pParam; ptr->SetWindowText(""); //等待兩個事件都被回置 WaitForMultipleObjects(2, global_Events, true, INFINITE); for(int i = 0;i<256;i++) { global_Array[i]=H; ptr->SetWindowText(global_Array); Sleep(10); } return 0; } void CEventDlg::OnBnClickedButtonStart() { for (int i = 0; i < 2; i++) { //例項化事件 global_Events[i]=CreateEvent(NULL,false,false,NULL); } CWinThread *ptrOne = AfxBeginThread(Global_ThreadOne, &m_One, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); ptrOne->ResumeThread(); //Start the second Thread CWinThread *ptrTwo = AfxBeginThread(Global_ThreadTwo, &m_Two, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); ptrTwo->ResumeThread(); //Start the Third Thread CWinThread *ptrThree = AfxBeginThread(Global_ThreadThree, &m_Three, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); ptrThree->ResumeThread(); // TODO: Add your control notification handler code here } |
事件可以實現不同程式中的執行緒同步操作,並且可以方便的實現多個執行緒的優先比較等待操作,例如寫多個WaitForSingleObject來代替WaitForMultipleObjects從而使程式設計更加靈活。
總結:
1. 互斥量與臨界區的作用非常相似,但互斥量是可以命名的,也就是說它可以跨越程式使用。所以建立互斥量需要的資源更多,所以如果只為了在程式內部是用的話使用臨界區會帶來速度上的優勢並能夠減少資源佔用量。因為互斥量是跨程式的互斥量一旦被建立,就可以通過名字開啟它。
2. 互斥量(Mutex),訊號燈(Semaphore),事件(Event)都可以被跨越程式使用來進行同步資料操作,而其他的物件與資料同步操作無關,但對於程式和執行緒來講,如果程式和執行緒在執行狀態則為無訊號狀態,在退出後為有訊號狀態。所以可以使用WaitForSingleObject來等待程式和執行緒退出。
3. 通過互斥量可以指定資源被獨佔的方式使用,但如果有下面一種情況通過互斥量就無法處理,比如現在一位使用者購買了一份三個併發訪問許可的資料庫系統,可以根據使用者購買的訪問許可數量來決定有多少個執行緒/程式能同時進行資料庫操作,這時候如果利用互斥量就沒有辦法完成這個要求,訊號燈物件可以說是一種資源計數器。