警告 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;
}
此程式碼的意圖如下:
- 從
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
內建函式