次の方法で共有


警告 C26837

関数 func の比較対象 comp の値は、volatile でない読み取りを通じて、宛先の場所 dest から読み込まれています。

この規則は、Visual Studio 2022 17.8 で追加されました。

解説

InterlockedCompareExchange 関数とその派生関数 (InterlockedCompareExchangePointer など) は、指定された値に対してアトミックな比較と交換の操作を実行します。 Destination 値が Comparand 値と等しい場合、exchange 値は 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 ポインターから読み取られ、スタック変数 lock に保存されます。 lock は以下のように 3 回使用されます。

  1. 最初は、最下位ビットが設定されているかどうかを確認するために。
  2. 2 回目は、InterlockedCompareExchange64 に対する Comparand 値として。
  3. 最後に、InterlockedCompareExchange64 からの戻り値の比較において。

これは、スタック変数に保存されている現在の値が関数の開始時に一度読み取られ、変化しないことを前提としています。 これが必要な理由は、現在の値がまず操作の試みの前にチェックされた後、InterlockedCompareExchange64 の中で Comparand として明示的に使用され、最後に InterlockedCompareExchange64 からの戻り値を比較するために使用されるためです。

残念ながら、上記のコードはソース コードから期待される動作とは異なる動作をするアセンブリにコンパイルされる可能性があります。 上記のコードを Microsoft Visual C++ (MSVC) コンパイラと /O1 オプションを使用してコンパイルし、結果のアセンブリ コードを調べて、lock への参照のそれぞれで 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 オプションを使用して、このコードをコンパイルします。 生成されたアセンブリは、lock 内にキャッシュされ値を使用するため plock を読み取らなくなりました。

コードの修正方法のその他の例については、「」を参照してください。

コード分析名: INTERLOCKED_COMPARE_EXCHANGE_MISUSE

コンパイラは、次のコードを lock 内にキャッシュされた値を使用する代わりに plock を複数回読み取るように最適化する可能性があります。

#include <Windows.h> 
 
bool TryLock(__int64* plock) 
{ 
    __int64 lock = *plock; 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
}

この問題を解決するには、コンパイラが、明示的に指示されない限りは同じメモリから連続して読み取りを行うようにコードを最適化しないように、読み取りを強制的に volatile にします。 これにより、オプティマイザーが予期しない動作を発生させることが回避されます。

メモリを volatile として扱う 1 つ目の方法は、以下のように宛先アドレスを volatile ポインターとして受け取ることです。

#include <Windows.h> 
 
bool TryLock(volatile __int64* plock) 
{ 
    __int64 lock = *plock; 
    return (lock & 1) && 
        _InterlockedCompareExchange64(plock, lock & ~1, lock) == lock; 
} 

2 つ目の方法は、宛先アドレスからの 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 値として使用されるかどうかを検出することによって行われます。 しかし、読み込まれた値が exchange 値を特定するために使用されているかどうかは明示的にチェックしません。 exchange 値が Comparand 値に関連していることを前提としています。

関連項目

InterlockedCompareExchange 関数 (winnt.h)
_InterlockedCompareExchange 組み込み関数