1、併發與同步
併發是指多個線程在同時執行:
單核(分時執行,並不是真正的同時執行)
多核(在某一個時刻會有多個線程在同時執行)
同步是保證在併發執行的環境中各個線程可以有序的執行
2、演示代碼
DWORD dwVal = 0; //全局變量
線程中的代碼:
dwVal ++; //安全嗎?
對應的彙編代碼:
mov eax, [0x12345678]
add eax, 1 //假設執行完這行代碼,系統時鐘到期,切換到其他線程,待其他線程執行完回來就會出問題
mov [0x12345678], eax
我們知道每個線程都有自己的堆棧,如果我們的變量都是局部變量,那麼久不存在併發問題
3、LOCK指令
把三行代碼換成一行代碼安全嗎?
inc DWORD PTR DS:[0x12345678]
仍然不安全,如果當前的環境是單核的,這樣的指令是安全的,因爲如果只有一個核就意味着某一時刻只有一個線程在執行這行代碼;但環境如果是多核的,在極端情況下就有可能兩個CPU同時執行這一行代碼產生意想不到的錯誤;
解決上面代碼問題,將上面代碼改成:
//使用LOCK鎖住的是當前代碼所在的那塊內存
LOCK inc DWORD PTR DS:[0x12345678]
可以參考:kernel32.InterLockedIncrement
原子操作相關API,主要包含在kernel32和ntdll中:
.text:7C809806 ; LONG __stdcall InterlockedIncrement(volatile LONG *lpAddend)
.text:7C809806 public _InterlockedIncrement@4
.text:7C809806 _InterlockedIncrement@4 proc near ; CODE XREF: CreatePipe(x,x,x,x)+57↓p
.text:7C809806 ; BasepCreateDefaultTimerQueue()+41↓p ...
.text:7C809806
.text:7C809806 lpAddend = dword ptr 4
.text:7C809806
.text:7C809806 mov ecx, [esp+lpAddend]
.text:7C80980A mov eax, 1
.text:7C80980F
.text:7C80980F loc_7C80980F: ; DATA XREF: .data:_BasepLockPrefixTable↓o
.text:7C80980F lock xadd [ecx], eax ; 關鍵代碼,前面加LOCK指令,xadd指令是先交換再加
.text:7C809813 inc eax ; 這裏再自加1,主要目的爲了返回
.text:7C809814 retn 4
.text:7C809814 _InterlockedIncrement@4 endp
4、多行代碼原子操作
上面的方法只能處理當行代碼,但是如果我們想要多行代碼執行原子操作呢?
關鍵代碼A //行代碼需要執行原子操作
關鍵代碼B //都加LOCK是否可行
關鍵代碼C
關鍵代碼有很多行,都加LOCK行不通;這樣我們就需要了解其他的解決方法。
5、臨界區:一次只允許一個線程進入直到該線程離開
DWORD dwFlag = 0; //實現臨界區的方式是加鎖
//鎖:全局變量進去+1,出來-1
if(dwFlag == 0){ //進入臨界區
dwFlag = 1;
......
.......
......
dwFlag = 0; //離開臨界區
}
如上代碼實現臨界區,任然不安全。
6、自己的方式實現臨界區
全局變量:Flag = 0
進入臨界區:
Lab:
mov eax,1 //eax先賦1
lock xadd [Flag], eax //交換並加1,如果Flag爲1,交換後加1就是2
cmp eax, 0 //eax和0比較
jz endLab //如果eax爲0就跳出並進入臨界區執行關鍵代碼
dec [Flag] //當Flag初始爲1纔會到這,Flag初始爲1會被加到2,所以這裏Flag需要減去1
//等待線程Sleep...
endLab:
ret
離開臨界區:
lock dec [Flag] //恢復Flag