350 likes | 514 Views
Thread Synchronization in User Mode. 如何處理多個 thread 合作同步的問題 ?. Outline. 出了甚麼問題 ? Without interrupt ( 保證 incrementing of the value is done atomically) Interlocked functions 簡介 InterLockedExchanged 使用範例 InterLockedChange 使用範例 InterLockedCompareExchage 簡介 Cache lines. 速度非常快.
E N D
Thread Synchronization in User Mode 如何處理多個thread 合作同步的問題?
Outline • 出了甚麼問題? • Without interrupt (保證 incrementing of the value is done atomically) • Interlocked functions簡介 • InterLockedExchanged 使用範例 • InterLockedChange 使用範例 • InterLockedCompareExchage 簡介 • Cache lines 速度非常快 Waste CPU time
Outline • Advanced Thread Synchronization • Critical Sections
出了甚麼問題? • 兩個 thread 同時存取一個global variable long g_x=0; DWORD WINAPI Thread1(PVOID pvParam){ g_x++; return(0); } DWORD WINAPI Thread2(PVOID pvParam){ g_x++; return(0); } 同時執行的造成 g_x 的結果不可預期! g_x++; 相當於 MOV EAX,[g_x] INC EAX MOV [g_x], EAX
同時執行g_x++; 有可能 MOV EAX,[g_x] INC EAX MOV EAX,[g_x] INC EAX MOV [g_x], EAX MOV [g_x], EAX EAX=0 EAX=1 EAX=0 EAX=1 g_x=1 g_x=1 Without interrupt (保證 incrementing of the value is done atomically) 全區域變數 組合語言指令交互執行 EAX 加一的結果, 還沒存起來就被蓋掉了 g_x++ 有必要不能被中斷!!
變數的位址 遞增的值(可以是負的) LONG InterlockedExchangeAdd( PLONG plAddend, LONG lIncrement); Interlocked functions簡介:InterlockedExchangeAdd() • Incrementing of the value is done atomically long g_x=0; DWORD WINAPI Thread1(PVOID pvParam){ InterlockedExchangeAdd( &g_x , 1); return(0); DWORD WINAPI Thread2(PVOID pvParam){ InterlockedExchangeAdd( &g_x , 1); return(0); } Atomically increase Atomically increase
InterLockedExchangedAdd使用範例 int APIENTRY WinMain(…){ HANDLE hThread[2]; DWORD dwThread1ID; hThread[0]=chBEGINTHREADEX(NULL,0,Thread1,(PVOID)NULL,0,&dwThread1ID); DWORD dwThread2ID; hThread[1]=chBEGINTHREADEX(NULL,0,Thread2,(PVOID)NULL,0,&dwThread2ID); WaitForMultipleObjects(2,hThread,TRUE,INFINITE); // 印出結果 TCHAR Message[100]; wsprintf(Message,_T("變數 g_x=%ld"),g_x); MessageBox(NULL,Message,_T("建立兩個 thread 存取變數 g_x"),MB_OK); // 關閉 Handle CloseHandle(hThread[0]); CloseHandle(hThread[1]); return 0; } 建立兩個 thread 存取變數 g_x 等待兩個 handle 全部都signaled才會繼續執行 避免: primary thread 結束,強迫所有 thread Exit InterlockedExcangeAdd 簡單範例
How do the interlocked functions work ? • Depends on the CPU • X86 assert a 硬體訊號 on the bus, 以防止其他 CPU 存取相同的記憶體位址 • InterlockedChangeAdd 特性 • 多個 CPU 亦能保證 atomic 性質 • 速度非常快 (less than 50 cycles) • 不會切換到 kernel model
欲更動變數的位址 傳回改變前的值 欲設定的值 LONG InterlockedExchange( PLONG plAddend, LONG lValue); g_fResourceInUse=TRUE 表示有人在使用 Interlocked functions簡介: InterlockedExChange() 設定 global 變數 Spin lock 在還沒有得到權限前, while 會一直耗盡 CPU time BOOL g_fResourceInUse=FALSE; … void Fun1(){ while( InterlockedExchange( & g_fResourceInUse, TRUE) == TRUE) Sleep(0); InterlockedExchange( & g_fResourceInUse, FALSE); } 等待進入存取資源 若之前是 TRUE, 則等待 放棄剩下的 time slices 進入 ready 狀態 存取資源 觧開 Spin lock 鎖
補充 全區域共用變數 g_fResourceInUse=FALSE; void ThreadFun1(){ while(g_fResourceInUse== TRUE) Sleep(0); // 設定進入 g_fResourceInUse=TRUE; // 設定離開 g_fResourceInUse=FALSE; } void ThreadFun2(){ while(g_fResourceInUse== TRUE) Sleep(0); // 設定進入 g_fResourceInUse=TRUE; // 設定離開 g_fResourceInUse=FALSE; } 存取共用檔案 (或資料) 存取共用檔案 (或資料) 防止兩條 thread 同時存取共用資料失敗 兩條 thread 同時都發現 g_fResourceInUse = FALSE 同時進入存取 設定與檢查必須同時進行
InterlockedExChange的注意事項 • Spin lock 非常浪費 CPU time • 多 CPUs 的情況下,欲存取的資料與 lock variable 在不同的cache line才會有效率(避免 race condition) cpu1 cpu2 Lock variable 共用資源 Cache line: 一般 CPU 執行程式會先把一小段程 式由 Memory 放到 Cache 中, 以增 進效率.
避免耗費大量 CPU time 的策略 • 讓給同樣 priority 等級的 thread 執行 • 若while 執行4000次,還沒搶到資源, 則進入 Kernel Mode 等待 (consuming no CPU time) while( InterlockedExchange( & g_fResourceInUse, TRUE) == TRUE) Sleep(0); 策略: 若搶不到資源, 則 sleep 一段時間. 若還是搶不到資源,則給予更長的時間. 比較好的策略是 Wait function WaitForSignalObject WaitForMultipleObjects
LONG InterlockedCompareExchange ( PLONG plDestination, LONG lExchange LONG lComparand); 1 (A) 2 (B) 3 (C) 訂正 Interlocked functions簡介: Atomic test and set operation 資料版本: 如果 long值 (*A) == long值 C, 則 (*A) = B (*1) 3 if( == ) else 不變 (*1) = 2 指標版本: 如果 位址 (*A) == 位址 C, 則 (*A) = B LONG InterlockedCompareExchangePointer ( PVOID* A, PVOID B PVOID C); (*A) C if( == ) else 不變 (*A)= B
32-byte 32-byte Cache lines 觀念 X86 CPU • 為了增進 CPU 執行效率, 一次會讀取 32 or 64-byte 到 cache中,並且 aligned on 32 or 64-byte boundary • 考量 multiprocessor CPU1 CPU2 Memory Update 問題 程式碼片段 若CPU1更改了其中一個變數的值, 必須通知 CPU2知道 複製 複製 複製 複製
Cache lines 衍生出的效率問題 • 應用程式中的資料應該以 • Cache-line 大小為單位 group 起來 • 並且放到 cache-line boundaries • ReadOnly 的變數與 Read/Write 變數分開 理由是: 不希望兩個 CPU 交互通知 寫就會通知
// 客戶資料 struct CustInfo { DWORD dwCustomerID; // 顧客號碼 int nBalanceDue; // 結餘款 char szName[100]; // 客戶名稱 FILETIME ftLastOrderDate; // 最後購物時間 }; Most Read Only // 客戶資料 struct CustInfo { DWORD dwCustomerID; // 顧客號碼 char szName[100]; // 客戶名稱 int nBalanceDue; // 結餘款 FILETIME ftLastOrderDate; // 最後購物時間 }; Read-Write 比較好的策略 Most Read Only Most Read Only Most Read Only Read-Write A. 強迫下面資料放在另一個 cache line Read-Write Read-Write B. 強迫以後的資料放在另一個 cache line Poor designed data structure
強迫把資料放在另一個 cache line #define CACHE_ALIGH 32 // 若是 X86 型系統 #define CACHE_PAD (Name, BytesSoFar) \ BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)] 1 計算前面的資料,剩下 的 Bytes 數目 X86 下, 一個 Cache-line 包含 32 bytes 2 只要配置 32- ((BytesSoFar)%32 Byte 的陣列就可以補滿了
完整的程式碼 // 客戶資料 struct CustInfo { DWORD dwCustomerID; // 顧客號碼 char szName[100]; // 客戶名稱 CACHE_PAD(X1,sizeof(DWORD)+100); int nBalanceDue; // 結餘款 FILETIME ftLastOrderDate; // 最後購物時間 CACHE_PAD(X2, sizeof(int)+sizeof(FILETIME)); }; Most Read Only Read-Write 有興趣的人,可以看網頁上的範例 Microsoft-specific storage-class attributes 資料成員的位址對齊新語法 __declspec(align(32))
我們需要一個機制,讓執行緒可以不用浪費 CPU時間 的等待存取一個共享的資源。
Critical Sections 概念 • 甚麼是Critical Section? • 是一程式區段, 而這個程式區段必須擁有某共用資源的權限才能執行 • 你可以放心的執行 Critical Section 的程式碼, 絕不會有其他的 thread 同時執行你所在的code • 你的 thread 會被 preempt 換其他的thread 執行, 但是想要進入 Critical Section 的thread 是不會被 schedule的 • 系統不保證進入Critical Section thread 的順序,但OS保證公平對待所有要進入的thread
DWORD WINAPI SecondThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { g_nIndex++; g_dwTimes[g_nIndex - 1] = GetTickCount(); } return(0); } 來看看, 不用Critical Section 會發生的問題 const int MAX_TIMES = 1000; int g_nIndex = 0; DWORD g_dwTimes[MAX_TIMES]; 存取共用資源 存取共用資源 第一個 thread DWORD WINAPI FirstThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { g_dwTimes[g_nIndex] = GetTickCount(); g_nIndex++; } return(0); } 第二個 thread
加入 Critical Section 解決問題 CRITICAL_SECTION g_cs; 注意: 取出位址 DWORD WINAPI FirstThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { EnterCriticalSection( &g_cs); EnterCriticalSection( &g_cs); g_dwTimes[g_nIndex] = GetTickCount(); g_nIndex++; LeaveCriticalSection( &g_cs); LeaveCriticalSection( &g_cs); } return(0); } 設定 Critical Section 起始 設定 Critical Section 結束 DWORD WINAPISecondThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { EnterCriticalSection( &g_cs); g_nIndex++; g_dwTimes[g_nIndex - 1] = GetTickCount(); LeaveCriticalSection( &g_cs); } return(0); }
存取共享資源的程式碼一定要用Critical Section 包起來 沒有使用Critical Section, 可以直接 存取共用資源 const int MAX_TIMES = 1000; int g_nIndex = 0; DWORD g_dwTimes[MAX_TIMES]; CRITICAL_SECTION g_cs; 違規 遵守規定 存取共用資源 第一個 thread DWORD WINAPI FirstThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { EnterCriticalSection( &g_cs); g_dwTimes[g_nIndex] = GetTickCount(); g_nIndex++; LeaveCriticalSection( &g_cs); } return(0); } 第二個 thread DWORD WINAPI SecondThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { g_nIndex++; g_dwTimes[g_nIndex - 1] = GetTickCount(); } return(0); }
我們來看看Critical Section 的處理細節 • 在使用 Critical Section 之前,要先作初始化的動作 • 初始化後, Process 中的thread 才能呼叫 • EnterCriticalSection(), TryEnterCriticalSection, LeaveCriticalSection() • 你不該去更改 Critical object的內容,你只要呼叫相關的function 操作即可 VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection ); 會 block return immediately
補充 Critical Section 的處理細節 • 當 thread 已經擁有 critical section後,亦可以呼叫 EnterCriticalSection() • 當擁有 critical section 的 thread 被 terminate掉時, critical section 的狀態沒有定義 • 若 critical section object 在呼叫 LeaveCriticalSection 之前就被 delete 掉,那麼那些在外面等待的 thread 其狀態沒有定義 防止等待自己的 dead lock 情事發生 呼叫 DeleteCriticalSection(&CriticalSection)
CRITICAL_SECTION g_cs; … WinMain(…) { InitializeCriticalSection( &g_cs); DeleteCriticalSection (&g_cs); } 建立第一個 thread 建立第二個 thread 所以你的程式架構,應該長成這樣 DWORD WINAPI FirstThread(PVOID pvParam) { bool bQuit=false; while(!bQuit){ EnterCriticalSection( &g_cs); if(q_nIndex<MAX_TIMES){ g_dwTimes[g_nIndex] = GetTickCount(); g_nIndex++; }else{ bQuit=true; } LeaveCriticalSection( &g_cs); } return(0); } Thread1.cpp WinMain.cpp DWORD WINAPISecondThread(PVOID pvParam) { bool bQuit=false; while (!bQuit) { EnterCriticalSection( &g_cs); if(q_nIndex<MAX_TIMES){ g_nIndex++; g_dwTimes[g_nIndex - 1] = GetTickCount(); }else{ bQui=true;} LeaveCriticalSection( &g_cs); } return(0); } Thread2.cpp CriticalSectionDemo
EnterCriticalSection 的處理流程 Yes 目前有其他的thread 正在存取資源 是否有執行緒正在存取資源 呼叫的 thread 被 blocked 住 NO 進入Waitting 狀態 (不會浪費CPU time) 更新 CriticalSection 中的變數,指明目前thread 已經存取資源 Return
饑餓(starve) 狀態 • 當一個 thread 一直無法得到 CPU 執行 • 你可以使用 BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs); • 等待進入critical section 的 thread 不會出現 starve • Time out 大約 30 天 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager Win2000 only 事先檢查是否可以進入Critical Section, 以免等待!!
進入 CriticalSetion 的流程 EnterCriticalSection DWORD WINAPI FirstThread(PVOID pvParam) { while (g_nIndex < MAX_TIMES) { EnterCriticalSection( &g_cs); g_dwTimes[g_nIndex] = GetTickCount(); g_nIndex++; LeaveCriticalSection( &g_cs); } return(0); } 在CriticalSection中,遞增1,表示目前有人在裡面 LeaveCriticalSection 在CriticalSection中,遞減1, =0 找出所有在外面等 待的thread,公平的 選一個進入排程
效率的考量: 合併 Critical Section 與 SPINLOCK • 當thread進入一個已經被佔用critical section Waiting State (由User mode 轉到 Kernel model) • 多CPU的情況 • 讓 thread 先用 spin lock 在 user mode 等待. 超過一定嘗試次數後, 才進入Kernel model waiting 很慢 耗費時間 (about 1000 CPU cycle) 單CPU 下, 你用 SPIN lock 等待一樣沒效率! 因為在自己的 time-slice 下沒使用完前,無法讓其他的 thread 進入critical section BOOL InitializeCriticalSectionAndSpinCount( PCRITICAL_SECTION pcs, DWORD dwSpinCount); single CPU下 spin count=0 與其空轉,不如讓出 time-slice 先讓別人執行 迴旋的次數
Useful Tips and Techniques 分開存取不同資源 int g_nNums[100]; CRITICAL_SECTION g_cs1; TCHAR g_cChars[100]; CRITICAL_SECTION g_cs2; DWORD WINAPI MyThread(PVOID pvParam) { EnterCriticalSection(& g_cs1); for (int x = 0; x < 100; x++) g_nNums[x] = 0; LeaveCriticalSection(& g_cs1); EnterCriticalSection(& g_cs2); for (x = 0; x < 100; x++) g_cChars[x] = TEXT('X'); LeaveCriticalSection(& g_cs2); return(0); } • 一個 share resource 只用一個 critical section保護. • 以免process 霸佔所有資源, 產生飢餓問題 共享資源1號 共享資源2號 1 2
當其他Thread 也要存取時,要注意順序一樣,才不會造成死結 DWORD WINAPI Other( …) { EnterCriticalSection(& g_cs2); EnterCriticalSection(& g_cs1); for (int x = 0; x < 100; x++) g_nNums[x] = g_cChars[x]; LeaveCriticalSection(& g_cs1); LeaveCriticalSection(& g_cs2); return(0); } 若要同時存取資源,怎麼辦? 同時存取資源 int g_nNums[100]; CRITICAL_SECTION g_cs1; TCHAR g_cChars[100]; CRITICAL_SECTION g_cs2; DWORD WINAPI MyThread(PVOID pvParam) { EnterCriticalSection(& g_cs2); EnterCriticalSection(& g_cs1); for (int x = 0; x < 100; x++) g_nNums[x] = g_cChars[x]; LeaveCriticalSection(& g_cs1); LeaveCriticalSection(& g_cs2); return(0); } 保護 共享資源1號 保護 共享資源2號 2 1
SOMESTRUCT g_s; CRITICAL_SECTION g_cs1; DWORD WINAPI MyThread(PVOID pvParam) { EnterCriticalSection(& g_cs1); SendMessage(hwndSomeWnd, WM_SOMEMSG, &g_s, 0); LeaveCriticalSection(& g_cs1); return(0); } Tip2: 進入CriticalSection 的時間不宜太久 • 若你要 SendMessage 給某個視窗 • 問題 • 在離開 critical section 前,沒有人可以存取 g_s; 1 不確定目標視窗何時會收到 Message SendMessage 會 block住
策略: 把資料複製起來,再送出去 SOMESTRUCT g_s; CRITICAL_SECTION g_cs1; DWORD WINAPI MyThread(PVOID pvParam) { EnterCriticalSection(& g_cs1); SOMESTRUCT sTemp = g_s; LeaveCriticalSection(& g_cs1); SendMessage(hwndSomeWnd, WM_SOMEMSG, & sTemp ,0); return(0); }