共用方式為


警告 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 內建函式