警告 C26837

函数 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; 
} 

此代码的意图是:

  1. plock 指针读取当前值。
  2. 检查此当前值是否设置了最小有效位。
  3. 如果它确实设置了最小有效位,请清除该位,同时保留当前值的其他位。

为此,请从 plock 指针读取当前值的副本并将其保存到堆栈变量 locklock 使用了三次,为了以下目的:

  1. 首先,用于检查最低有效位是否已设置。
  2. 其次,用作 InterlockedCompareExchange64Comparand 值。
  3. 最后,用在 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 内部函数