Win32線程安全問題.同步函數
線程安全問題.同步函數
一丶簡介什麽是線程安全
通過上面幾講.我們知道了線程怎麽創建.線程切換的原理(CONTEXT結構) 每個線程在切換的時候都有自己的堆棧.
但是這樣會有安全問題. 為什麽? 我們每個線程都使用自己的局部變量這個是沒有安全問題的. 但是線程可能會使用全局變量.這樣很有可能會產生安全問題.為什麽是很有可能.
1.有全局變量的情況下.有可能會有安全問題.
2.對全局變量進行寫操作.則一定有安全問題.
上面兩個條件都具備,線程才是不安全的.
為什麽是不安全的.
試想一下. 如果這個全局變量在更改.另一個線程也更改了.最後則會出現兩個線程同時更改這個全局變量. 問題就會出現在這.
例如以下代碼:
// 臨界區同步函數.cpp : 定義控制臺應用程序的入口點。 // #include "stdafx.h" #include <Windows.h> DWORD g_Number = 10; DWORD WINAPI MyThreadFun1(LPVOID lParame) { while (g_Number > 0) { printf("+++剩下Number個數 = %d\r\n", g_Number); g_Number--; printf("+++當前的Number個數 = %d\r\n", g_Number); } return 0; } DWORD WINAPI MyThreadFun2(LPVOID lParame) { while (g_Number > 0 ) { printf("***剩下Number個數 = %d\r\n", g_Number); g_Number--; //產生線程安全問題 printf("***當前的Number個數 = %d\r\n", g_Number); } return0; } int main(int argc,char *argv[]) { HANDLE hThreadHand[2] = { NULL }; hThreadHand[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, 0, NULL); hThreadHand[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, 0, NULL); //創建兩個線程 WaitForMultipleObjects(2, hThreadHand, TRUE, INFINITE); printf("Number個數 = %d \r\n", g_Number); system("pause"); return 0; }
上面的代碼很簡單. 看下運行結果
為什麽會產生這個問題.原因是.在線程中我們有個地方
while(全局變量 > 0) 則會執行下邊代碼. 但是很有可能執行完這一句. 線程發生了切換. 去執行另一個線程去了. 最終會產生這樣的結果.
如果看反匯編.則會發現 全局變量--的地方.匯編代碼 並不是一局. 如果發生線程切換則會出現錯誤.
首先獲取全局變量的值.
然後sub -1
最後重新賦值.
很有可能在sun eax 1的時候就發生了切換. 這樣就有安全問題了.為了解決這些問題.我們必須想辦法. 所以Windows提供了一組線程同步的函數.
二丶線程同步函數之臨界區
什麽時候臨界區. 臨界區的意思就是 這一個區域我給你鎖定.當前有且只能有一個線程來執行我們臨界區的代碼.
而臨界資源是一個全局變量
臨界區的使用步驟.
1.創建全局原子變量.
2.初始化全原子變量
3.進入臨界區
4.釋放臨界區.
5.刪除臨界區.
具體API:
1.全局原子變量
CRITICAL_SECTION g_cs; //直接創建即可.不用關心內部實現.
2.初始化全局原子變量.InitializeCriticalSection
_Maybe_raises_SEH_exception_ VOID InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //傳入全局原子變量的地址
);
3.使用的API 進入臨界區.
void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection //全局原子變量 );
下面還有一個. 是嘗試無阻塞模式進入臨界區. 意思就是內部加了一個判斷.是否死鎖了.
BOOL TryEnterCriticalSection( 返回吃持有的臨界區對象.如果成功的情況下.
LPCRITICAL_SECTION lpCriticalSection
);
4.使用API 釋放臨界區.
void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection //全局原子對象 );
5.刪除臨界區對象.
void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
代碼例子:
// 臨界區同步函數.cpp : 定義控制臺應用程序的入口點。 // #include "stdafx.h" #include <Windows.h> //創建臨界區結構 CRITICAL_SECTION g_cs; DWORD g_Number = 10; DWORD WINAPI MyThreadFun1(LPVOID lParame) { EnterCriticalSection(&g_cs); //進入臨界區 while (g_Number > 0) { printf("+++剩下Number個數 = %d\r\n", g_Number); g_Number--; printf("+++當前的Number個數 = %d\r\n", g_Number); } LeaveCriticalSection(&g_cs); return 0; } DWORD WINAPI MyThreadFun2(LPVOID lParame) { EnterCriticalSection(&g_cs); //進入臨界區 while (g_Number > 0 ) { printf("***剩下Number個數 = %d\r\n", g_Number); g_Number--; //while語句內就是臨界區了.有且只能一個線程訪問. printf("***當前的Number個數 = %d\r\n", g_Number); } LeaveCriticalSection(&g_cs); return 0; } int main(int argc,char *argv[]) { //初始化臨界區全局原子變量 InitializeCriticalSectionAndSpinCount(&g_cs, 0x00000400); //InitializeCriticalSection(&g_cs); //初始化臨界區.兩個API都可以. HANDLE hThreadHand[2] = { NULL }; hThreadHand[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun1, NULL, 0, NULL); hThreadHand[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MyThreadFun2, NULL, 0, NULL); //創建兩個線程 WaitForMultipleObjects(2, hThreadHand, TRUE, INFINITE); DeleteCriticalSection(&g_cs); //刪除臨界區. printf("+Number個數 = %d \r\n", g_Number); system("pause"); return 0; }
官方MSDN例子:
鏈接: https://docs.microsoft.com/zh-cn/windows/desktop/Sync/using-critical-section-objects
三丶線程同步之互斥體
1.臨界區缺點.以及衍生出來的跨進程保護
上面講了臨界區. 但是我們的臨界資源是一個全局變量.例如下圖:
如果我們的臨界資源是一個文件. 需要兩個進程都要訪問怎麽辦? 此時臨界區已經不可以跨進程使用了.
2.跨進程控制.
跨進程控制就是指 不同進程中的多線程控制安全..比如A進程訪問臨界資源的時候. B進程不能訪問. 因為臨界區的 令牌.也就是我們說的全局原子變量.只能在應用層.
但是如果放到內核中就好辦了. 如下圖所示
A進程的線程從內核中獲取互斥體. 為0 還是為1. B進程一樣. 如果為 0 則可以進行訪問臨界資源. 訪問的時候.互斥體則設置為1(也就是令牌設置為1)這樣B進程就獲取不到了.自然不能訪問
臨界區資源了.
3.互斥體操作API
既然明白了互斥體是一個內核層的原子操作.那麽我們就可以使用API 進行操作了.
操作步驟.
1.創建互斥體. 信號量設置為有信號的狀態 例如全局的原子變量現在是有信號.是可以進行訪問的.
2.獲取信號狀態. 如果有信號則進入互斥體臨界區執行代碼.此時互斥體信號為無信號. 也就是說別的進程訪問的時候.因為沒有信號.執行不了代碼.
3.釋放互斥體. 信號狀態為有信號. 此時別的進程信號已經有了.所以可以進行訪問了.
具體API:
1.創建互斥體
HANDLE CreateMutexA(
LPSECURITY_ATTRIBUTES lpMutexAttributes, SD安全屬性.句柄是否可以繼承.每個內核對象API都擁有.
BOOL bInitialOwner, 初始的信號量狀態. false為有信號. 獲取令牌的時候可以獲取到. True為無信號. 且如果為True互斥體對象為線程擁有者.
LPCSTR lpName 全局名字. 根據名字尋找互斥體.
);
2.獲取令牌.
DWORD WaitForSingleObject(
HANDLE hHandle, 等待的內核對象
DWORD dwMilliseconds 等待的時間
);
調用此函數之後.信號為無信號.別的進程是進入不了互斥體臨界區的.
3.釋放互斥體
BOOL ReleaseMutex(
HANDLE hMutex
);
調用完比之後.互斥體為有信號.可以使用了.
代碼例子:
兩個工程代碼是一樣的.貼一份出來.
#include "stdafx.h" #include <Windows.h> //創建臨界區結構 int main(int argc,char *argv[]) { //初始化臨界區全局原子變量 HANDLE MutexHandle = CreateMutex(NULL, FALSE, TEXT("AAA")); //創建互斥體. 信號量為0. 有信號的狀態.wait可以等待 WaitForSingleObject(MutexHandle,INFINITE); for (size_t i = 0; i < 10; i++) { Sleep(1000); printf("A進程訪問臨街資源中臨街資源ID = %d \r\n", i); } ReleaseMutex(MutexHandle); return 0; }
先運行A進程在運行B進程. 則B進程處於卡死狀態.
實現了同步. 除非A進程釋放互斥體句柄使信號變為有信號.此時才可以訪問B
官方代碼例子:
#include <windows.h> #include <stdio.h> #define THREADCOUNT 2 HANDLE ghMutex; DWORD WINAPI WriteToDatabase( LPVOID ); int main( void ) { HANDLE aThread[THREADCOUNT]; DWORD ThreadID; int i; // Create a mutex with no initial owner ghMutex = CreateMutex( NULL, // default security attributes FALSE, // initially not owned 有信號 NULL); // unnamed mutex 不需要跨進程使用.所以不用名字 if (ghMutex == NULL) { printf("CreateMutex error: %d\n", GetLastError()); return 1; } // Create worker threads for( i=0; i < THREADCOUNT; i++ ) { aThread[i] = CreateThread( //創建 THREADCOUNT個線程 NULL, // default security attributes 0, // default stack size (LPTHREAD_START_ROUTINE) WriteToDatabase, NULL, // no thread function arguments 0, // default creation flags &ThreadID); // receive thread identifier if( aThread[i] == NULL ) { printf("CreateThread error: %d\n", GetLastError()); return 1; } } // Wait for all threads to terminate WaitForMultipleObjects(THREADCOUNT, aThread, TRUE, INFINITE); //等待線程執行完畢 // Close thread and mutex handles for( i=0; i < THREADCOUNT; i++ ) CloseHandle(aThread[i]); CloseHandle(ghMutex); return 0; } DWORD WINAPI WriteToDatabase( LPVOID lpParam ) { // lpParam not used in this example UNREFERENCED_PARAMETER(lpParam); DWORD dwCount=0, dwWaitResult; // Request ownership of mutex. while( dwCount < 20 ) { dwWaitResult = WaitForSingleObject( //線程內部等待互斥體.因為一開始為FALSE所以有信號.第一次執行線程的時候則會執行. ghMutex, // handle to mutex INFINITE); // no time-out interval switch (dwWaitResult) { // The thread got ownership of the mutex case WAIT_OBJECT_0: __try { // TODO: Write to the database printf("Thread %d writing to database...\n", GetCurrentThreadId()); dwCount++; } __finally { // Release ownership of the mutex object if (! ReleaseMutex(ghMutex)) //執行完畢.釋放互斥體.信號量變為有信號. 其余線程等待的時候可以等到則可以繼續執行線程代碼 { // Handle error. } } break; // The thread got ownership of an abandoned mutex // The database is in an indeterminate state case WAIT_ABANDONED: return FALSE; } } return TRUE; }
Win32線程安全問題.同步函數