使用 xchg vs cmpxchg 获取锁
就英特尔处理器的性能而言,它是相同的,但为了简单起见,让事情更容易理解,我更喜欢你给出的例子中的第一种方式。如果您可以使用xchg 执行此操作,则没有理由使用cmpxchg 获取锁。
根据奥卡姆剃刀原理,越简单越好。
除此之外,使用xchg 锁定更强大 - 您还可以检查软件逻辑的正确性,即您没有访问尚未明确分配用于锁定的内存字节。因此,您将检查您是否使用了正确初始化的同步变量。除此之外,您将能够检查您没有解锁两次。
使用普通内存存储来释放锁
对于在释放锁时写入同步变量是否应该只使用普通内存存储(mov)或总线锁定内存存储(即具有隐式或显式lock的指令)没有达成共识-前缀,如xchg。
使用普通内存存储释放锁的方法是 Peter Cordes 推荐的,详情参见下面的 cmets。
但是在某些实现中,获取和释放锁都是通过总线锁定内存存储完成的,因为这种方法似乎简单直观。例如,Windows 10 下的 LeaveCriticalSection 使用总线锁定存储即使在单插槽处理器上也能释放锁定;而在具有非统一内存访问 (NUMA) 的多个物理处理器上,这个问题更加重要。
我在单插槽 CPU (Kaby Lake) 上对执行大量内存分配/重新分配/释放的内存管理器进行了微基准测试。当没有争用时,即线程数少于物理内核时,锁定释放测试完成速度会慢约 10%,但当物理内核有更多线程时,锁定释放测试完成速度会快 2%。因此,平均而言,释放锁的普通内存存储优于锁定内存存储。
检查同步变量有效性的锁定示例
查看这个更安全的锁定函数示例(Delphi 编程语言),它检查同步变量的数据的有效性,并捕获释放未获取的锁的尝试:
const
cLockAvailable = 107; // arbitrary constant, use any unique values that you like, I've chosen prime numbers
cLockLocked = 109;
cLockFinished = 113;
function AcquireLock(var Target: LONG): Boolean;
var
R: LONG;
begin
R := InterlockedExchange(Target, cLockByteLocked);
case R of
cLockAvailable: Result := True; // we've got a value that indicates that the lock was available, so return True to the caller indicating that we have acquired the lock
cLockByteLocked: Result := False; // we've got a value that indicates that the lock was already acquire by someone else, so return False to the caller indicating that we have failed to acquire the lock this time
else
begin
raise Exception.Create('Serious application error - tried to acquire lock using a variable that has not been properly initialized');
end;
end;
end;
procedure ReleaseLock(var Target: LONG);
var
R: LONG;
begin
// As Peter Cordes pointed out (see comments below), releasing the lock doesn't have to be interlocked, just a normal store. Even for debugging we use normal load. However, Windows 10 uses locked release on LeaveCriticalSection.
R := Target;
Target := cLockAvailable;
if R <> cLockByteLocked then
begin
raise Exception.Create('Serious application error - tried to release a lock that has not been actually locked');
end;
end;
您的主应用程序放在这里:
var
AreaLocked: LONG;
begin
AreaLocked := cLockAvailable; // on program initialization, fill the default value
....
if AcquireLock(AreaLocked) then
try
// do something critical with the locked area
...
finally
ReleaseLock(AreaLocked);
end;
....
AreaLocked := cLockFinished; // on program termination, set the special value to catch probable cases when somebody will try to acquire the lock
end.
高效的基于暂停的自旋等待循环
测试、测试和设置
您还可以使用以下汇编代码(请参阅下面的“基于暂停的自旋等待循环的汇编代码示例”部分)作为“基于暂停”的自旋等待循环的工作示例。
按照 Peter Cordes 的建议,此代码在旋转时使用正常的内存负载来节省资源。这种技术被称为“测试、测试和设置”。您可以在https://stackoverflow.com/a/44916975/6910868
了解有关此技术的更多信息
迭代次数
本例中基于暂停的自旋等待循环首先尝试通过读取同步变量来获取锁,如果它不可用,则在 5000 个循环的循环中使用pause 指令。 5000 次循环后,它调用 Windows API 函数 SwitchToThread()。 5000 次循环的这个值是经验值。它基于我的测试。从 500 到 50000 的值似乎也可以,但在某些情况下,较低的值更好,而在其他情况下,较高的值更好。您可以在我在上一段中提供的 URL 上阅读更多关于基于暂停的自旋等待循环的信息。
暂停指令的可用性
请注意,您只能在支持 SSE2 的处理器上使用此代码 - 您应该在调用 pause 指令之前检查相应的 CPUID 位 - 否则只会浪费功率。在没有pause 的处理器上,只需使用其他方法,例如 EnterCriticalSection/LeaveCriticalSection 或 Sleep(0),然后在循环中使用 Sleep(1)。有人说,在 64 位处理器上,您可能不会检查 SSE2 以确保执行 pause 指令,因为原始 AMD64 架构采用 Intel 的 SSE 和 SSE2 作为核心指令,实际上,如果您运行 64-位代码,您肯定已经拥有 SSE2 以及 pause 指令。但是,英特尔不鼓励依赖存在特定功能的做法,并明确指出某些功能可能会在未来的处理器中消失,应用程序必须始终通过 CPUID 检查功能。然而,SSE 指令变得无处不在,许多 64 位编译器在不检查的情况下使用它们(例如用于 Win64 的 Delphi),因此在某些未来的处理器中没有 SSE2 的可能性非常小,更不用说pause。
基于暂停的自旋等待循环的汇编代码示例
// on entry rcx = address of the byte-lock
// on exit: al (eax) = old value of the byte at [rcx]
@Init:
mov edx, cLockByteLocked
mov r9d, 5000
mov eax, edx
jmp @FirstCompare
@DidntLock:
@NormalLoadLoop:
dec r9
jz @SwitchToThread // for static branch prediction, jump forward means "unlikely"
pause
@FirstCompare:
cmp [rcx], al // we are using faster, normal load to not consume the resources and only after it is ready, do once again interlocked exchange
je @NormalLoadLoop // for static branch prediction, jump backwards means "likely"
lock xchg [rcx], al
cmp eax, edx // 32-bit comparison is faster on newer processors like Xeon Phi or Cannonlake.
je @DidntLock
jmp @Finish
@SwitchToThread:
push rcx
call SwitchToThreadIfSupported
pop rcx
jmp @Init
@Finish: