int i=0;
i++;
printf("%d\n",i);
对于单CPU,开两个线程的话,如果不使用锁 最终的i可能不是2
对应的汇编是
1 movl $0 i(%rip)
2 movl i(%rip) %eax
3 addl $1 %eax
4 movl %eax i(%rip)
可见i++ 不是原子的
线程有属于自己的CPU指令和寄存器,但内存是共享的,
可理解为A,B为房客,每人搬进去之前,上一个人的东西要搬出来,腾出地方,再给下一个住, 线程也这样
线程1执行下3的时候,由于时间片的原因, 线程1被换出,CPU中寄存器的内容(1)被清0,线程2进入CPU,一直执行到4,i值为1,线程2 由于时间片,被换出,线程进入cpu,CPU的寄存器内容还是1,写入内存,最终i值为1
线程1 线程2
1 movl $0 i(%rip)
2 movl i(%rip) %eax
3 addl $1 %eax
(由于时间片,被换出,同时CPU相应寄存器内容保存其他地方,同时清0)
1 movl $0 i(%rip)
2 movl i(%rip) %eax
3 addl $1 %eax
4 movl %eax i(%rip) //此时i为2
4 movl %eax i(%rip) //线程1获得CPU,将之前保存到别的地方的寄存器内容放回去,再写回内存,但i为1
这时要加锁了
1 movl $0 i(%rip)
线程1 线程
lock
2 movl i(%rip) %eax
3 addl $1 %eax
4 movl %eax i(%rip)
unlock
lock
2 movl i(%rip) %eax
3 addl $1 %eax
4 movl %eax i(%rip)
unlock
假设线程1先获得了锁,那么 把变量i所在内存的数据 load到寄存器,再对寄存器数据加1,再将结果写回变量i所在内容中 ,此时i为1
在上面任何一个步骤终,线程2去尝试加锁将失败
当线程1解锁后,线程去加锁,还是重复上面的步骤,最终i为2
虽然线程之间加锁能保证数据的原子性,但耗性能,线程切换也就是清空寄存器,清除缓存,但对于单CPU来说也只能这样了 可使用GCC本身支持的原子函数
若是多CPU呢
线程1指定到CPU1上,线程指定到CPU2上, 这时就没有加锁的必要性了,但若执行上面的程序,i 的值,还是有可能不为2
如果说单CPU中不为2,是由于线程切换,寄存器清0产生的,那么多CPU中i不为2,就是由于CPU之间的缓存不一致产生的
我们知道CPU不会直接跟内存打交道的,CPU是直接跟缓存打交道的
对于int i=0对应的汇编 movl $0 i(%rip) cpu拿到这个指令后,发现其内存地址不在缓存中,就去内存中load 大小为64字节的数据(cpu缓存局部性)
cpu1 cpu2
(1) int i=0 缓存 0 缓存0
(2) i++ 缓存 1 缓存1
为了让CPU之间通信,需要对bus加锁,也就是锁住内存, 当cpu1执行i++ 时,锁总线,这时cpu2是能感知到的,它会意思到它缓存中相应数据非法,等待
当cpu对bus解锁后,cpu2也是能感知到的,再从内存中load相应数据到cpu2的缓存中,再进行计算,这时i的值为2了
对bus加锁,会使得其他cpu不能使用内存,即读取不相关的数据也不行,性能差
就改为锁缓存,不再锁总线了
缓存一致性 MESI
当CPU1执行到 (2)时, 发一个指令给CPU2,告诉它,它的缓存数据是invalid,CPU1需要重新读内存 , 同时CPU1将i设置为exclusive,进行i++后,修改状态为modify
当CPU2打算从内存读i时,CPU1能感知到的,马上将 i的状态 置为share,并写回内存,CPU2从内存读取到i的值,已经为1了,再i++,最终i的值为2
不管是锁总线,还是锁缓存,都是CPU内部提供的指令,比软件锁要快多了
cpu内部提供的锁 在gcc中对应的函数是xchg , cmpxchg(CAS)
cpu 1 cpu2
i++ 发出指令,设置i状态为exlusive 收到指令后,意识到自己缓存区中的数据失效invalid
同时执行i++,将状态置为modify 马上去读取内存
感觉到cpu2要读取内存,设置状态为
share,并写回内存
读内存
cpu2中准备去读内存和真正读内存的间隔很小的,