函数
func
的比较数comp
的值已通过非易失性读取从目标位置dest
加载。
此规则是在 Visual Studio 2022 17.8 中添加的。
备注
InterlockedCompareExchange
函数及其派生函数(例如 InterlockedCompareExchangePointer
)会对指定值执行原子形式的比较和交换运算。 如果 Destination
值等于 Comparand
值,交换值将存储在由 Destination
指定的地址。 否则,不会执行任何操作。 interlocked
函数提供了一种简单的机制,用于同步对多个线程共享的变量的访问权限。 相对于其他 interlocked
函数的调用而言,该函数是原子函数。 滥用这些函数可能会生成行为不同于预期的对象代码,因为优化可能会以意想不到的方式更改代码的行为。
考虑下列代码:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
此代码的意图是:
- 从
plock
指针读取当前值。 - 检查此当前值是否设置了最小有效位。
- 如果它确实设置了最小有效位,请清除该位,同时保留当前值的其他位。
为此,请从 plock
指针读取当前值的副本并将其保存到堆栈变量 lock
。 lock
使用了三次,为了以下目的:
- 首先,用于检查最低有效位是否已设置。
- 其次,用作
InterlockedCompareExchange64
的Comparand
值。 - 最后,用在
InterlockedCompareExchange64
提供的返回值的比较中
这假设保存到堆栈变量的当前值在函数开始时被读取一次,并且不会更改。 这是必要的,因为在尝试运算之前首先会检查当前值,然后将其显式用作 InterlockedCompareExchange64
中的 Comparand
,最后将其用于比较来自 InterlockedCompareExchange64
的返回值。
遗憾的是,前面的代码可以编译成行为与你期望的源代码不同的程序集。 使用 Microsoft Visual C++ (MSVC) 编译器和 /O1
选项编译前面的代码,并检查生成的汇编代码以了解如何获取每个对 lock
的引用的锁值。 MSVC 编译器版本 v19.37 生成的汇编代码如下所示:
plock$ = 8
bool TryLock(__int64 *) PROC ; TryLock, COMDAT
mov r8b, 1
test BYTE PTR [rcx], r8b
je SHORT $LN3@TryLock
mov rdx, QWORD PTR [rcx]
mov rax, QWORD PTR [rcx]
and rdx, -2
lock cmpxchg QWORD PTR [rcx], rdx
je SHORT $LN4@TryLock
$LN3@TryLock:
xor r8b, r8b
$LN4@TryLock:
mov al, r8b
ret 0
bool TryLock(__int64 *) ENDP ; TryLock
rcx
保存参数 plock
的值。 汇编代码每次都从 plock
重新读取值,而不是复制堆栈上的当前值。 这意味着每次读取的值可能都不相同。 这会使得开发人员正在执行的清理工作无效。 在验证该值已设置最低有效位后,将从 plock
重新读取该值。 由于执行此验证后会重新读取它,因此新值可能不再设置最低有效位。 在争用条件下,此代码的行为可能就像它已被另一个线程锁定时成功获取了指定的锁一样。
只要代码的行为不改变,编译器就可以删除或添加内存读取或写入。 为了防止编译器进行此类更改,当你从内存读取值并将其缓存在变量中时,请强制将读取设置为 volatile
。 声明为 volatile
的对象不会在某些优化中使用,因为它们的值可能随时更改。 生成的代码在请求 volatile
对象时始终读取该对象的当前值,即使前面的指令要求从同一对象获取值。 反之亦然(出于同样的原因)。 除非进行了请求,否则不会再次读取 volatile
对象的值。 有关 volatile
的更多信息,请参见volatile
。 例如:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *static_cast<volatile __int64*>(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
与以前一样使用 /O1
选项编译此代码。 生成的程序集不再读取 plock
以使用 lock
中的缓存值。
有关如何修复代码的更多示例,请参阅示例。
代码分析名称:INTERLOCKED_COMPARE_EXCHANGE_MISUSE
示例
编译器可能会优化以下代码以多次读取 plock
,而不是使用 lock
中的缓存值:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
若要解决此问题,请强制将读取设置为 volatile
,使编译器不会优化代码以从同一内存进行连续读取,除非存在明确的指示。 这可以防止优化器引入意外行为。
若要将内存视为 volatile
,第一种方法是将目标地址视为 volatile
指针:
#include <Windows.h>
bool TryLock(volatile __int64* plock)
{
__int64 lock = *plock;
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
第二种方法是使用从目标地址读取的 volatile
。 可通过多种不同的方式执行此操作:
- 在取消引用此指针之前将此指针强制转换为
volatile
指针 - 从提供的指针创建
volatile
指针 - 使用
volatile
读取帮助程序函数。
例如:
#include <Windows.h>
bool TryLock(__int64* plock)
{
__int64 lock = ReadNoFence64(plock);
return (lock & 1) &&
_InterlockedCompareExchange64(plock, lock & ~1, lock) == lock;
}
启发
强制执行此规则的方法是:检测 InterlockedCompareExchange
函数或它的任何派生函数的 Destination
中的值是否通过非 volatile
读取进行加载,然后将其用作 Comparand
值。 但是,它不会明确检查加载的值是否用于确定交换值。 它假定交换值与 Comparand
值相关。
另请参阅
InterlockedCompareExchange
函数 (winnt.h)
_InterlockedCompareExchange
内部函数