并发错误
多线程带来效率的提高,但是也带来了对原子操作的破坏。
例如
C代码 | 汇编代码 |
i++; | mov eax,[i] inc eax mov [i],eax |
假设i=0; 两个线程执行后应该为2;
如果按照如下方式执行结果为1
线程1 | 线程2 |
mov eax,[i]; | 等待 |
等待 | mov eax,[i]; |
等待 | Inc eax; |
等待 | mov [i],eax; |
inc eax; | 等待 |
mov [i],eax; | 等待 |
原子操作
如果是单处理器使用一条指令完成即可实现原子操作Inc [i];
如果是多处理器inc[i]; 依然不是原子的;使用lock 指令前缀锁定内存后才能保证指令的原子性 lock inc [i];
自旋锁中的原子操作
从上面可以看出,对数据的原子操作,直接是硬件支持的,如果操作的数据比较复杂要实现对这个复杂数据的原子操作就需要硬件和软件一起配合来完成。具体方法如下:
设有变量l 为0表示已经加锁,1 表示未加锁
加锁代码如下:
1: lock decb [l]
jns 3
2: rep nop
cmpb 0, [l]
jle 2
jmp 1
3: ….(排它性操作代码操作)
Lock incb [l]
如果未加锁:的执行路径为: 执行完1 跳转到3继续执行后面的逻辑。
如果已经加锁的 执行路径为:执行完1,因跳转条件不成立,就执行2,并在2中不停的循环检查,直到锁释放(l为1)跳转到1重新执行,直到具备执行3中的逻辑,这样就保证了3中代码的原子性,在3中可以执行复杂的数据操作。
信号量中的原子操作
设有信号量sc初始为0。
申请操作如下:
Lock decl [sc] // 对信号量减1操作
Js 2 // 信号量小于0
1: ….(申请成功执行的代码)
2: lea eax, [sc] //
Call 申请信号量失败函数 //(进入内核态睡眠,有信号量从内核态返回)
Jmp 1 //
释放操作如下:
Lock incl [sc] // 对信号量加1操作
Jle 2 //小于有需要唤醒的线程
1: 信号量释放成功的代码逻辑
2: lea eax, [sc]
Call 唤醒阻塞的线程(进入内核态)
Jmp 1