list :: empty()マルチスレッドの動作?


9

さまざまなスレッドが要素を取得するためのリストがあります。リストが空のときに保護しているmutexをロックしないようにするために、ロックするempty()前にチェックします。

呼び出しがlist::empty()100%正しくない場合でも問題ありません。同時list::push()およびlist::pop()コールのクラッシュまたは中断を回避したいだけです。

私はVC ++とGnu GCCが時々empty()間違って、何も悪いことはないと思い込んでも大丈夫ですか?

if(list.empty() == false){ // unprotected by mutex, okay if incorrect sometimes
    mutex.lock();
    if(list.empty() == false){ // check again while locked to be certain
         element = list.back();
         list.pop_back();
    }
    mutex.unlock();
}

1
いいえ、これを想定することはできません。VCのconcurrent_queueの
Panagiotis Kanavos

2
@Fureeishこれは答えになるはずです。これは、std::list::size一定の時間の複雑さを保証していることを付け加えます。これは、基本的に、サイズ(ノード数)を別の変数に格納する必要があることを意味します。それを呼ぼうsize_std::list::empty次にsize_ == 0、何かがとして返される可能性が高く、の同時読み取りと書き込みsize_によりデータ競合が発生するため、UBになります。
Daniel Langr

@DanielLangr「一定時間」はどのように測定されますか?単一の関数呼び出しまたは完全なプログラムのどちらにありますか?
curiousguy

1
@curiousguy:DanielLangrは「リストノードの数に依存しない」という質問に答えました。これは、O(1)の正確な定義です。つまり、要素の数に関係なく、すべての呼び出しが一定の時間未満で実行されます。en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions他のオプション(C ++ 11まで)は線形= O(n)の意味になります。つまり、サイズは要素(リンクリスト)を数える必要があり、並行性(カウンターでの非アトミックな読み取り/書き込みよりも明らかなデータ競合)。
firda

1
@curiousguy:dVを使用して独自の例をとると、時間の複雑さは同じ計算です-制限。これらすべてのものは、再帰的に定義されるか、「Nごとにf(N)<CであるようなCが存在する」という形式で定義されます。アルゴは、任意の入力でC時間未満で終了すること)償却とは、平均すると、一部の入力の処理に時間がかかる可能性があることを意味します(例:再ハッシュ/再割り当てが必要)。ただし、平均は一定です(可能なすべての入力を想定)。
firda

回答:


10

呼び出しがlist::empty()100%正しくない場合でも問題ありません。

いいえ、大丈夫ではありません。同期メカニズム(ミューテックスのロック)の外でリストが空かどうかを確認すると、データ競合が発生します。データの競合があると、未定義の動作が発生します。未定義の動作があるということは、プログラムについてもはや推論できず、得られる出力はすべて「正しい」ことを意味します。

健全性を重視する場合は、パフォーマンスヒットを取得し、チェックする前にミューテックスをロックします。そうは言っても、リストはあなたにとって正しいコンテナではないかもしれません。あなたがそれを使って何をしているのかを正確に知らせることができれば、より良いコンテナを提案できるかもしれません。


個人的な視点は、呼び出しがlist::empty()とは何の関係もありませんreadアクションであるrace-condition
ゴックカングエン

3
@NgọcKhánhNguyễnリストに要素を追加している場合は、サイズの書き込みと読み取りを同時に行っているため、間違いなくデータ競合が発生します。
NathanOliver

6
@NgọcKhánhNguyễnそれは間違っています。競合状態はread-writeまたはwrite-writeです。私を信じないなら、データレースの標準セクションを読んでください
NathanOliver

1
@NgọcKhánhNguyễn:書き込みも読み取りもアトミックであることが保証されていないため、同時に実行できるため、読み取りが完全に間違ったものになる可能性があります(破損した読み取りと呼ばれます)。リトルエンディアン8ビットMCUで0x00FFを0x0100に変更する書き込みを想像してください。最初に下位0xFFを0x00に書き換えると、読み取りは正確にそのゼロを取得し、両方のバイトを読み取り(書き込みスレッドの遅延または中断)、上位バイトを0x01に更新して書き込みを続行します。しかし、読み取りスレッドはすでに間違った値を取得しています(0x00FFでも0x0100でもないが、予期しない0x0000)。
firda

1
@NgọcKhánhNguyễn一部のアーキテクチャでは、C ++仮想マシンでそのような保証はありません。あなたのハードウェアがそうしたとしても、スレッド同期がない限りそれが単一のスレッドだけを実行していると想定してそれに応じて最適化できるので、コンパイラが変更を決して見ない方法でコードを最適化することは合法です。
NathanOliver '10 / 10/19

6

互いに同期ていない読み取りと書き込み(おそらく、そのような名前が付けられていると仮定すると、のsizeメンバーへの書き込みstd::list)があります。一方のスレッドが(外部で)呼び出し、もう一方のスレッドが内部に入り、実行されると想像してください。次に、変更されている可能性のある変数を読み取っています。これは未定義の動作です。empty()if()if()pop_back()


2

物事がうまくいかない例:

十分にスマートなコンパイラはmutex.lock()list.empty()戻り値を変更できない可能性があるため、内部ifチェックを完全にスキップし、最終的pop_backに、最初のの後に最後の要素が削除されたリストのにつながる可能性がありますif

なぜそれができるのですか?では同期は行われないlist.empty()ため、同時に変更された場合、データ競合が発生します。標準では、プログラムにはデータ競合がないものとするため、コンパイラーはそれを当然のことと見なします(それ以外の場合、最適化はほとんど行われません)。したがって、非同期のシングルスレッドの視点を想定して、list.empty()一定に保つ必要があると結論付けることができます。

これは、コードを破壊する可能性のあるいくつかの最適化(またはハードウェアの動作)の1つにすぎません。


現在のコンパイラーは最適化したくもないようですa.load()+a.load()...
curiousguy

1
@curiousguyどのように最適化しますか?そこで完全な順次一貫性を要求すると、それが得られます...
Max Langhof

@MaxLanghofあなたは最適化a.load()*2が明白だとは思わない?a.load(rel)+b.load(rel)-a.load(rel)とにかく最適化されていません。何もありません。なぜロック(本質的にはほとんどがseq整合性を持っている)がより最適化されることを期待するのですか?
curiousguy

@curiousguy非アトミックアクセス(ここではロックの前と後)とアトミックのメモリ順序が完全に異なるためですか?ロックが「より多く」最適化されることは期待していません。非同期アクセスは、順次一貫したアクセスよりも最適化されることを期待しています。ロックの存在は私のポイントには関係ありません。そして、いや、コンパイラは最適化することが許可されていませんa.load() + a.load()2 * a.load()。詳しく知りたい場合は、遠慮なく質問してください。
Max Langhof

@MaxLanghof私はあなたが何を言おうとしているのか全くわかりません。ロックは基本的に順次一貫しています。なぜ実装は、一部のスレッド化プリミティブ(ロック)で最適化を試み、他のいくつか(アトミック)では最適化を行わないのですか?アトミック以外のアクセスがアトミックの使用に関して最適化されることを期待していますか?
curiousguy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.