1 / 35

Thread Synchronization in User Mode

Thread Synchronization in User Mode. 如何處理多個 thread 合作同步的問題 ?. Outline. 出了甚麼問題 ? Without interrupt ( 保證 incrementing of the value is done atomically) Interlocked functions 簡介 InterLockedExchanged 使用範例 InterLockedChange 使用範例 InterLockedCompareExchage 簡介 Cache lines. 速度非常快.

henry
Download Presentation

Thread Synchronization in User Mode

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Thread Synchronization in User Mode 如何處理多個thread 合作同步的問題?

  2. Outline • 出了甚麼問題? • Without interrupt (保證 incrementing of the value is done atomically) • Interlocked functions簡介 • InterLockedExchanged 使用範例 • InterLockedChange 使用範例 • InterLockedCompareExchage 簡介 • Cache lines 速度非常快 Waste CPU time

  3. Outline • Advanced Thread Synchronization • Critical Sections

  4. 出了甚麼問題? • 兩個 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

  5. 同時執行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++ 有必要不能被中斷!!

  6. 變數的位址 遞增的值(可以是負的) 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

  7. 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 簡單範例

  8. 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

  9. 欲更動變數的位址 傳回改變前的值 欲設定的值 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 鎖

  10. 補充 全區域共用變數 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 同時進入存取 設定與檢查必須同時進行

  11. InterlockedExChange的注意事項 • Spin lock 非常浪費 CPU time • 多 CPUs 的情況下,欲存取的資料與 lock variable 在不同的cache line才會有效率(避免 race condition) cpu1 cpu2 Lock variable 共用資源 Cache line: 一般 CPU 執行程式會先把一小段程 式由 Memory 放到 Cache 中, 以增 進效率.

  12. 避免耗費大量 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

  13. 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

  14. 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知道 複製 複製 複製 複製

  15. Cache lines 衍生出的效率問題 • 應用程式中的資料應該以 • Cache-line 大小為單位 group 起來 • 並且放到 cache-line boundaries • ReadOnly 的變數與 Read/Write 變數分開 理由是: 不希望兩個 CPU 交互通知 寫就會通知

  16. // 客戶資料 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

  17. 強迫把資料放在另一個 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 的陣列就可以補滿了

  18. 完整的程式碼 // 客戶資料 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))

  19. 我們需要一個機制,讓執行緒可以不用浪費 CPU時間 的等待存取一個共享的資源。

  20. Critical Sections 概念 • 甚麼是Critical Section? • 是一程式區段, 而這個程式區段必須擁有某共用資源的權限才能執行 • 你可以放心的執行 Critical Section 的程式碼, 絕不會有其他的 thread 同時執行你所在的code • 你的 thread 會被 preempt 換其他的thread 執行, 但是想要進入 Critical Section 的thread 是不會被 schedule的 • 系統不保證進入Critical Section thread 的順序,但OS保證公平對待所有要進入的thread

  21. 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

  22. 加入 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); }

  23. 存取共享資源的程式碼一定要用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); }

  24. 我們來看看Critical Section 的處理細節 • 在使用 Critical Section 之前,要先作初始化的動作 • 初始化後, Process 中的thread 才能呼叫 • EnterCriticalSection(), TryEnterCriticalSection, LeaveCriticalSection() • 你不該去更改 Critical object的內容,你只要呼叫相關的function 操作即可 VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection ); 會 block return immediately

  25. 補充 Critical Section 的處理細節 • 當 thread 已經擁有 critical section後,亦可以呼叫 EnterCriticalSection() • 當擁有 critical section 的 thread 被 terminate掉時, critical section 的狀態沒有定義 • 若 critical section object 在呼叫 LeaveCriticalSection 之前就被 delete 掉,那麼那些在外面等待的 thread 其狀態沒有定義 防止等待自己的 dead lock 情事發生 呼叫 DeleteCriticalSection(&CriticalSection)

  26. 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

  27. EnterCriticalSection 的處理流程 Yes 目前有其他的thread 正在存取資源 是否有執行緒正在存取資源 呼叫的 thread 被 blocked 住 NO 進入Waitting 狀態 (不會浪費CPU time) 更新 CriticalSection 中的變數,指明目前thread 已經存取資源 Return

  28. 饑餓(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, 以免等待!!

  29. 進入 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,公平的 選一個進入排程

  30. 效率的考量: 合併 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 先讓別人執行 迴旋的次數

  31. 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

  32. 當其他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

  33. 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住

  34. 策略: 把資料複製起來,再送出去 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); }

  35. End

More Related