11

Herb Sutter の講演: C++ and Beyond 2012: Herb Sutter - atomic<> Weapons, 2 of 2

彼は std::shared_ptr デストラクタの実装にバグを示しています:

if( control_block_ptr->refs.fetch_sub(1, memory_order_relaxed ) == 0 )
    delete control_block_ptr; // B

彼は、memory_order_relaxed により、delete を fetch_sub の前に配置できると述べています。

1:25:18 - 離すとライン B が本来あるべき位置に維持されない

それはどのように可能ですか?どちらもシングルスレッドであるため、事前発生/事前シーケンスの関係があります。私が間違っているかもしれませんが、fetch_sub と delete の間にも Carry-a-dependency-to があります。

彼が正しければ、どの ISO 項目がそれをサポートしていますか?

4

4 に答える 4

2

共有ポインターを解放するコードを想像してください。

auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();

dec_ref() に「リリース」セマンティックがない場合、コンパイラ (または CPU) が dec_ref() の前から後に物事を移動することはまったく問題ありません (たとえば)。

auto tmp = &(the_ptr->a);
the_ptr.dec_ref();
*tmp = 10;

dec_ref() は他のスレッドから同時に呼び出され、オブジェクトを削除する可能性があるため、これは安全ではありません。したがって、dec_ref() がそこにとどまる前に、「リリース」セマンティックが必要です。

オブジェクトのデストラクタが次のようになっていると想像してみましょう。

~object() {
    auto xxx = a;
    printf("%i\n", xxx);
}

また、例を少し変更して、2 つのスレッドを作成します。

// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
the_ptr.dec_ref();

// thread 2
the_ptr.dec_ref();

次に、「集約された」コードは次のようになります。

// thread 1
auto tmp = &(the_ptr->a);
*tmp = 10;
{ // the_ptr.dec_ref();
    if (0 == atomic_sub(...)) {
        { //~object()
            auto xxx = a;
            printf("%i\n", xxx);
        }
    }
}

// thread 2
{ // the_ptr.dec_ref();
    if (0 == atomic_sub(...)) {
        { //~object()
            auto xxx = a;
            printf("%i\n", xxx);
        }
    }
}

ただし、atomic_sub() の「リリース」セマンティックしかない場合、このコードは次のように最適化できます。

// thread 2
auto xxx = the_ptr->a; // "auto xxx = a;" from destructor moved here
{ // the_ptr.dec_ref();
    if (0 == atomic_sub(...)) {
        { //~object()
            printf("%i\n", xxx);
        }
    }
}

しかし、そのようにすれば、デストラクタは常に "a" の最後の値を出力するとは限りません (このコードはもうレースフリーではありません)。そのため、atomic_sub のセマンティックを取得する必要もあります (厳密には、デクリメント後にカウンターが 0 になったときにバリアを取得する必要があります)。

于 2015-02-10T01:07:03.680 に答える
1

彼は共有オブジェクト自体に対するアクションの同期について話しているように見えますが、これは彼のコードブロックには表示されていません(その結果、混乱を招きます)。

それが彼が言った理由acq_relです-オブジェクトに対するすべてのアクションは、オブジェクトが破壊される前に、すべて順番に行われる必要があるためです。

deleteしかし、なぜ彼がとの交換について話しているのかはまだわかりませんfetch_sub

于 2013-02-14T19:23:10.563 に答える
1

返信が遅くなりました。

この単純な型から始めましょう。

struct foo
{
    ~foo() { std::cout << value; }
    int value;
};

そして、次のように、この型を ashared_ptrで使用します。

void runs_in_separate_thread(std::shared_ptr<foo> my_ptr)
{
    my_ptr->value = 5;
    my_ptr.reset();
}

int main()
{
    std::shared_ptr<foo> my_ptr(new foo);
    std::async(std::launch::async, runs_in_separate_thread, my_ptr);
    my_ptr.reset();
}

2 つのスレッドが並行して実行され、両方ともfooオブジェクトの所有権を共有します。

正しいshared_ptr実装 (つまり、 を持つものmemory_order_acq_rel) では、このプログラムは動作を定義しています。このプログラムが出力する唯一の値は5.

不適切な実装 (を使用memory_order_relaxed) では、そのような保証はありません。のデータ競合が発生するため、動作は未定義です foo::value。この問題は、デストラクタがメイン スレッドで呼び出された場合にのみ発生します。メモリの順序が緩いfoo::value場合、他のスレッドへの書き込みがメイン スレッドのデストラクタに反映されない場合があります。以外の値5が出力される可能性があります。

では、データ競合とは何ですか? 定義を確認して、最後の箇条書きに注意してください。

式の評価がメモリ位置に書き込み、別の評価が同じメモリ位置を読み取りまたは変更する場合、式は競合すると言われます。2 つの相反する評価があるプログラムには、データ競合が発生します。

  • 競合する評価はどちらもアトミック操作です (std::atomic を参照)。
  • 競合する評価の 1 つが別の評価の前に発生する (std::memory_order を参照)

このプログラムでは、1 つのスレッドが に書き込み、foo::value1 つのスレッドが から読み取りますfoo::value。これらはシーケンシャルであると想定されています。への書き込みfoo::valueは常に読み取りの前に行う必要があります。直観的には、デストラクタはオブジェクトに最後に発生するものであると想定されているため、それらが存在することは理にかなっています。

memory_order_relaxedただし、そのような注文保証は提供していないため memory_order_acq_rel、必須です。

于 2016-07-07T06:57:45.660 に答える
0

Herb が示したトークでは、 ではmemory_order_releaseありませんmemory_order_relaxedが、リラックスするとさらに多くの問題が発生します。

delete control_block_ptrアクセスしない限りcontrol_block_ptr->refs(おそらくそうではないでしょう)、アトミック操作は削除に依存関係を持ちません。削除操作は、制御ブロック内のメモリに触れず、そのポインタを freestore アロケータに戻すだけかもしれません。

しかし、ハーブがアトミック操作の前に削除を移動するコンパイラについて話しているのか、それとも副作用が他のスレッドに見えるようになったときについて言及しているのかはわかりません。

于 2013-02-14T18:53:16.453 に答える