【C】原子操作InterlockedIncrement并非lock inc
如果是lock inc的话,inc把被操作数加上1以后就扔了,并不会存到寄存器。如果你要取回被操作数被加上1后的值的话,你还得再读一次内存,然而这个期间被操作数的值很可能就被修改了。这是一个常见谬误。原子操作是需要通过取回加减前后的值来判断锁没锁住(或者别的),所以光是把一个变量“完美地”搞一下,并不能达到要求。
MSDN上的某博客给出了一个非原子操作的解决方案,那就是一边操作一边循环比对看变量是不是自己变化了,从而判断是否被别的线程改写了。然而它看起来十分不靠谱。因为这会让牙膏厂处理器只弄一下缓存里的数据后就跑了,对于多处理器平台或者对MAP到内存地址上的外设(典型例子:VESA显存)的控制而言,很糟糕。目测连内存(或者外设)都摸不到。
所以事实上肯定是有个交换的过程的。必须要先算出新值,再取回旧值,然后把旧值返回。
我在VS2012上用Debug单步走了一下发现,InterlockedIncrement的实现是这样的:mov ecx,dword ptr
mov eax,1
lock xadd dword ptr ,eax
inc eax
ret 4其中的lock xadd起到了决定性的原子操作,把目标操作数加上eax(也就是加上1),然后取得目标操作数旧值。最后inc一下就得到了加上1之后的值并返回。
InterlockedDecrement也一样,只不过加了个-1然后dec了eax而已。
顺带一提,InterlockedExchange并非lock xchg,它是有个循环的。mov ecx,dword ptr
mov edx,dword ptr
mov eax,dword ptr
the_loop:
lock cmpxchg dword ptr ,edx
jne the_loop
ret 8根据牙膏厂手册的描述:Exchanges the contents of the destination (first) and source (second) operands. The operands can be two general-purpose registers or a register and a memory location.
If a memory operand is referenced, the processor’s locking protocol is automatically implemented for the duration of the exchange operation, regardless of the presence or absence of the LOCK prefix or of the value of the IOPL. (See the LOCK prefix description in this chapter for more information on the locking protocol.)
This instruction is useful for implementing semaphores or similar data structures for process synchronization. (See “Bus Locking” in Chapter 8 of the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, for more information on bus locking.)
翻译:
交换俩操作数。你既可以进行俩通用寄存器之间的交换,也可以进行寄存器到内存之间的交换。如果你要做寄存器到内存的交换的话,处理器会自动锁总线,不管你有没有lock前缀并且无视你的IOPL的值。(看这个章节的LOCK前缀了解锁总线协议)
这玩意儿在实现多线程同步的信号或者类似玩意儿的时候很有用。(在牙膏厂手册3A卷第八章节看“锁总线”了解锁总线。)
啊,原来xchg指令并不需要lock前缀呀。早说嘛。咦,那xchg岂不是天生适合做原子操作了么!微软干嘛要循环lock cmpxchg呢?
且看牙膏厂描述cmpxchg指令:This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically. To simplify the interface to the processor’s bus, the destination operand receives a write cycle without regard to the result of the comparison. The destination operand is written back if the comparison fails; otherwise, the source operand is written into the destination. (The processor never produces a locked read without also producing a locked write.)
翻译:这玩意儿可以配合一个LOCK前缀来运行一次原子操作。为了简化处理器总线接口,不管比较结果怎么样,目标操作数都会收到一个写入周期。比较的结果决定了它把哪个操作数写入目标操作数。(处理器不可能锁了后只读取却不写入。)
划掉的我曾认为是废话,后面发现这个写入过程会导致缓存刷新,而别的核心如果也在争用这个原子锁,别的核心就要刷新自己的缓存——导致其重新从RAM读取一个4KB的页。多个核心高强度争用一个原子锁,会因为缓存刷新的问题严重拖慢整个系统的速度,这是TAS原子锁的特性。
先说一下cmpxchg这个指令到底是干啥的:
1、比较eax与目标操作数的值。(根据长度,可以是al ax eax rax)
2、eax等于目标操作数时:把源操作数写入目标操作数。
3、eax不等于目标操作数:把目标操作数读入eax。
所以划掉的“废话”的意思是在第三条所说情况下因为不想改变目标操作数的值,所以把之前读出的目标操作数的值写回。
根据InterlockedExchange的实现,eax和目标操作数都是dword ptr 。所以它就是想要把edx拿去和dword ptr 交换。如果dword ptr 变化了则重新交换,这也是循环的由来。交换好了后,eax就是所需的返回值。
虽然比xchg啰嗦,但这是一种提高性能的表现。为啥呢?因为xchg总是锁总线,会锁住整个内存,但lock cmpxchg则不会在没必要的情况下产生没必要的锁。 在支持多CPU和不支持多CPU(只支持单CPU多核多线程)的情况下,原子操作锁总线的行为都是不一样的。有的是真的锁了总线,有的则只是进行了缓存内的操作。
页:
[1]