(67)多核同步,lock 总线锁 ,自己实现临界区
一、多核同步問題
單條匯編指令可能被多個CPU同時執行,此時就可能會引發安全問題。
考慮下面的指令:
INC DWORD PTR DS:[0x12345678]如果兩個CPU同時執行該指令,[0x12345678] 的初始值是0,那么兩個CPU執行后,本應該 [0x12345678] 是2,結果卻有可能是1。
為了解決這個問題,可以使用 LOCK 對某個內存地址“加鎖”,將指令修改成如下:
LOCK INC DWORD PTR DS:[0x12345678]添加 LOCK 之后,保證了多個CPU不能同時對這條指令進行訪問,這也就實現了安全保證。
單核模式下,單條匯編指令一定是滿足原子性的,所以根本用不到 LOCK ,我們可以驗證一下這個說法,首先了解一下 WINDOWS 提供的原子性操作API。
二、原子操作API
這里列舉部分:
- InterlockedIncrement
- InterlockedExchangeAdd
- InterlockedDecrement
- InterlockedFlushSList
- InterlockedExchange
- InterlockedPopEntrySList
- InterlockedCompareExchange
- InterlockedPushEntrySList
先挑一個 InterlockedIncrement 作簡單介紹,這個API可以在三環使用的,作用是對某個變量+1,滿足原子性,就是說不會有多核或者多線程的同步安全問題。它接收一個地址作為參數,對它里面的值+1,然后返回+1 后的結果,這個宏的聲明我們也是可以找到的:
LONG InterlockedIncrement(LPLONG lpAddend // variable to increment );下面我們就來對比看看單核和多核模式下,這個API的實現有什么區別。
首先我們打開單核模式的內核文件,找到 InterlockedIncrement ,觀察其代碼:
; __fastcall __InterlockedIncrement(x) public @__InterlockedIncrement@4 @__InterlockedIncrement@4 proc near mov eax, 1 xadd [ecx], eax inc eax retn @__InterlockedIncrement@4 endpxadd是先交換兩個數,然后把求和的結果存到原來的第一個操作數里,結合上面的介紹,這個函數應該不難理解。
然后打開多核模式下的內核文件,我說明一下怎么找多核內核文件和符號文件:
ntoskrnl - 單處理器,不支持PAE
ntkrnlpa - 單處理器,支持PAE
ntkrnlmp - 多處理器,不支持PAE
ntkrpamp - 多處理器,支持PAE
我們在符號文件的目錄下能找到符號文件,比如我要多核PAE,那就是 ntkrpamp.pdb ,然后把虛擬機設置改成多核,去 system32 里取 ntkrnlpa.exe ,就好了。
打開內核文件,載入符號,找到 InterlockedIncrement
; __fastcall __InterlockedIncrement(x) public @__InterlockedIncrement@4 @__InterlockedIncrement@4 proc near mov eax, 1 lock xadd [ecx], eax inc eax retn @__InterlockedIncrement@4 endp發現和單核的區別就是 xadd 指令加了 lock 。這樣就保證了多個CPU不能同時讀這個指令的內存,也就不能同時執行該指令了,這也就保證了對 [ecx] 的同步訪問。
三、自己實現臨界區(不使用xadd)
所謂臨界區,可以理解成某段指令同一時刻只能有一個CPU/線程在執行,實現方法是多樣的。
比如我可以定義一個全局變量 CriticalLock 初始化為 -1,表示當前沒有占用,可以訪問臨界區。
每個線程的第一條指令都是 inc CriticalLock ,然后判斷如果等于0,表示自己是第一個線程,就可以執行業務代碼;否則就算沒搶到,把 CriticalLock 減回去,循環剛才的步驟直到 CriticalLock 等于0.
聽起來沒什么問題,下面給出一段代碼,起了10個線程,每個線程給全局變量 g_value 加1,重復十次,理論上最后 g_value 應該等于 100.
錯誤的實現
#include "stdafx.h" #include <windows.h>int g_value = 0;int CriticalLock = -1; // -1表示可以進入臨界區DWORD WINAPI MyThread(LPVOID TID) {for (int i = 0; i < 10; i++){ CriStart:Sleep(20); // 提高效率__asm{inc [CriticalLock];jz CriEnd;dec [CriticalLock];jmp CriStart;} CriEnd:// 耗時業務g_value+=3;Sleep(20);g_value-=2;printf("%d\n", g_value);__asm dec [CriticalLock];}return 0; }int _tmain(int argc, _TCHAR* argv[]) {for (int i = 0; i < 10; i++){CreateThread(0,0,MyThread,(LPVOID)i,0,0);}getchar();printf("所有線程結束,g_value = %d\n", g_value);getchar();return 0; }可以看到程序輸出是錯的,原因是當前機器是多核的,多個CPU同時調用不同的線程,同時對 CrititalLock 進行讀寫,就會出現同步安全問題。(這段代碼在單核模式下是正確的)
錯誤的實現2
#include "stdafx.h" #include <windows.h>int g_value = 0;int CriticalLock = -1; // -1表示可以進入臨界區DWORD WINAPI MyThread(LPVOID TID) {for (int i = 0; i < 10; i++){ CriStart:Sleep(20); // 提高效率__asm{lock inc [CriticalLock];jz CriEnd;lock dec [CriticalLock];jmp CriStart;} CriEnd:// 耗時業務g_value+=3;Sleep(20);g_value-=2;printf("%d\n", g_value);__asm lock dec [CriticalLock];}return 0; }int _tmain(int argc, _TCHAR* argv[]) {for (int i = 0; i < 10; i++){CreateThread(0,0,MyThread,(LPVOID)i,0,0);}getchar();printf("所有線程結束,g_value = %d\n", g_value);getchar();return 0; }正確的實現(xchg)
#include "stdafx.h" #include <windows.h>int g_value = 0;int CriticalLock = 0;DWORD WINAPI MyThread(LPVOID TID) {for (int i = 0; i < 10; i++){ CriStart:__asm{mov eax,1;lock xchg [CriticalLock],eax;cmp eax,1;jnz CriEnd;}Sleep(20);__asm jmp CriStart; CriEnd:// 耗時業務g_value+=3;Sleep(20);g_value-=2;printf("%d\n", g_value);__asm dec [CriticalLock];}return 0; }int _tmain(int argc, _TCHAR* argv[]) {for (int i = 0; i < 10; i++){CreateThread(0,0,MyThread,(LPVOID)i,0,0);}getchar();printf("所有線程結束,g_value = %d\n", g_value);getchar();return 0; } 《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的(67)多核同步,lock 总线锁 ,自己实现临界区的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (66)全局句柄表,遍历全局句柄表
- 下一篇: (68)自旋锁 , cmpxchg8b