3

1 つのプロデューサーによって書き込まれ、N 人のコンシューマーによって読み取られるリングバッファーがあります。これはリングバッファであるため、プロデューサーによって書き込まれるインデックスがコンシューマーの現在の最小インデックスよりも小さくても問題ありません。プロデューサーとコンシューマーの位置は、それぞれの によって追跡されCursorます。

class Cursor
{
public:
    inline int64_t Get() const { return iValue; }
    inline void Set(int64_4 aNewValue)
    {
        ::InterlockedExchange64(&iValue, aNewValue);
    }

private:
    int64_t iValue;
};

//
// Returns the ringbuffer position of the furthest-behind Consumer
//
int64_t GetMinimum(const std::vector<Cursor*>& aCursors, int64_t aMinimum = INT64_MAX)
{
    for (auto c : aCursors)
    {
        int64_t next = c->Get();
        if (next < aMinimum)
        {
            aMinimum = next;
        } 
    }

    return aMinimum;
}

生成されたアセンブリ コードを見ると、次のように表示されます。

    mov rax, 922337203685477580   // rax = INT64_MAX
    cmp rdx, rcx    // Is the vector empty?
    je  SHORT $LN36@GetMinimum
    npad    10
$LL21@GetMinimum:
    mov r8, QWORD PTR [rdx]    // r8 = c
    cmp QWORD PTR [r8+56], rax // compare result of c->Get() and aMinimum
    cmovl   rax, QWORD PTR [r8+56] // if it's less then aMinimum = result of c->Get()
    add rdx, 8                 // next vector element
    cmp rdx, rcx    // end of the vector?
    jne SHORT $LL21@GetMinimum
$LN36@GetMinimum:
    fatret  0   // beautiful friend, the end

コンパイラが の値を読み取ってc->Get()と比較しaMinimum、条件付きで の RE-READ 値を に移動してc->Get()も問題ないと判断する方法がわかりませんaMinimum。私の考えでは、この値はcmpcmovl命令の間で変更された可能性があります。私が正しければ、次のシナリオが可能です。

  • aMinimum現在は 2 に設定されています

  • c->Get()1 を返します

  • これcmpが完了し、less-thanフラグが設定されます

  • 別のスレッドが、現在のスレッドが現在保持している値cを 3 に更新します。

  • cmovlaMinimum3に設定

  • プロデューサは 3 を認識し、リングバッファの位置 2 のデータがまだ処理されていなくても上書きします。

私はあまりにも長い間それを見てきましたか?次のようなものではないでしょうか。

mov rbx, QWORD PTR [r8+56]
cmp rbx, rax 
cmovl rax, rbx 
4

1 に答える 1

3

へのアクセスの周りにアトミックまたはあらゆる種類のスレッド間シーケンス操作を使用していないためiValue(おそらく、別のスレッドで変更している可能性のあるものについても同じことが当てはまりますがiValue、それは問題ではないことがわかります)、コンパイラはコードの 2 つのアセンブリ行の間で変更されないままであると想定するのは自由です。別のスレッドが変更iValueすると、未定義の動作が発生します。

コードがスレッドセーフであることを意図している場合は、アトミック、ロック、またはシーケンス操作を使用する必要があります。

C++11 標準では、セクション 1.10「マルチスレッド実行とデータ競合」でこれを形式化していますが、これは特に読みやすいものではありません。この例に関連する部分は次のとおりだと思います。

パラグラフ 10:

次の場合、評価 A は評価 B の前に依存順序付けされます。

  • A がアトミック オブジェクト M に対して解放操作を実行し、別のスレッドで B が M に対して消費操作を実行し、A が先頭にある解放シーケンスの副作用によって書き込まれた値を読み取る、または
  • ある評価 X では、A は X の前に依存関係の順序で並べられ、X は B に依存関係を持ちます。

Cursor::Get()評価 A が関数に対応し、評価 B が を変更する目に見えないコードに対応すると言う場合iValue。評価 A ( Cursor::Get()) は、アトミック オブジェクトに対して操作を実行せず、依存関係が他の何よりも優先されることはありません (したがって、ここには「X」は含まれません)。

そして、評価 A が を変更するコードに対応し、評価iValueB が に対応すると言えばCursor::Get()、同じ結論を導き出すことができます。Cursor::Get()したがって、 と の修飾子の間に「前に依存順序付けられた」関係はありませんiValue

したがって、Cursor::Get()変更される可能性のあるものの前に依存関係が順序付けられているわけではありませんiValue

パラグラフ 11:

次の場合、評価 A がスレッド間で評価 B の前に発生します。

  • A が B と同期する、または
  • A が B の前に依存順序付けされている、または
  • いくつかの評価 X
    • A が X と同期し、X が B の前にシーケンスされる、または
    • A が X の前にシーケンスされ、X が B の前にスレッド間で発生する、または
    • X の前にスレッド間が発生し、B の前に X のスレッド間が発生します。

繰り返しますが、これらの条件はどれも満たされていないため、以前にスレッド間が発生することはありません。

パラグラフ12

次の場合、評価 A は評価 B の前に発生します。

  • A が B の前に配列されている、または
  • B の前にスレッド間が発生します。

どちらの操作も「スレッド間で発生する」ことはありません。また、"sequenced before" という用語は、1.9/13 "Program execution" で、単一のスレッドで発生する評価にのみ適用されるものとして定義されています ("sequenced before" は、古い "sequence point" 用語の C++11 の置き換えです)。別々のスレッドでの操作について話しているので、A を B の前に並べることはできません。

Cursor::Get()したがって、この時点で、別のスレッドで発生する変更の「前に発生」しないことがわかりiValueます (逆も同様です)。最後に、パラグラフ 21 でこれの結論に達します。

プログラムの実行にデータ競合が含まれているのは、異なるスレッドに競合する 2 つのアクションが含まれており、そのうちの少なくとも 1 つがアトミックではなく、どちらも他のスレッドの前に発生しない場合です。このようなデータ競合は、未定義の動作を引き起こします。

したがって、あるスレッドで使用Cursor::Get()し、別のスレッドで何かを変更するiValue場合は、未定義の動作を回避するために、アトミックまたはその他のシーケンス操作 (mutex など) を使用する必要があります。

標準によれば、volatileスレッド間のシーケンスを提供するには十分ではないことに注意してください。Microsoft のコンパイラ、明確に定義されたスレッド間の動作をサポートするためにいくつかの追加の約束を提供する場合がありますが、そのサポートは構成可能であるため、新しいコードvolatileに依存することを避けることをお勧めします。volatileこれについて MSDN が述べていることの一部を次に示します ( http://msdn.microsoft.com/en-us/library/vstudio/12a04hfd.aspx )。

ISO準拠

C# volatile キーワードに精通している場合、または以前のバージョンの Visual C++ での volatile の動作に精通している場合は、C++11 ISO 標準の volatile キーワードが異なり、/volatile:iso の場合に Visual Studio でサポートされることに注意してください。コンパイラオプションが指定されています。(ARM の場合、デフォルトで指定されています)。C++11 ISO 標準コードの volatile キーワードは、ハードウェア アクセスにのみ使用されます。スレッド間通信には使用しないでください。スレッド間通信には、C++ 標準テンプレート ライブラリの std::atomic などのメカニズムを使用します。

マイクロソフト固有

/volatile:ms コンパイラ オプションを使用すると (既定では、ARM 以外のアーキテクチャが対象となる場合)、コンパイラは、他のグローバル オブジェクトへの参照の順序を維持するだけでなく、揮発性オブジェクトへの参照間の順序を維持するための追加のコードを生成します。特に:

  • 揮発性オブジェクトへの書き込み (揮発性書き込みとも呼ばれます) にはリリース セマンティクスがあります。つまり、命令シーケンスで揮発性オブジェクトへの書き込みの前に発生するグローバルまたは静的オブジェクトへの参照は、コンパイルされたバイナリでの揮発性書き込みの前に発生します。

  • 揮発性オブジェクトの読み取り (揮発性読み取りとも呼ばれます) には、取得セマンティクスがあります。つまり、命令シーケンスで揮発性メモリの読み取り後に発生するグローバルまたは静的オブジェクトへの参照は、コンパイルされたバイナリでの揮発性読み取りの後に発生します。

これにより、揮発性オブジェクトをマルチスレッド アプリケーションでのメモリのロックと解放に使用できるようになります。

于 2013-02-06T02:14:56.847 に答える