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