マルチスレッド アプリケーションがあり、同じ入力で実行するとします。書き込みと書き込み、書き込みと読み取りのデータ競合を検出するために、すべてのロードとストアを計測するだけで十分ですか? つまり、ログに記録されたロード アドレスとストア アドレスから、どのスレッドがどのロードを行い、どのスレッドがどのストアを行ったかがわかれば、重複したアドレスに注目することで、書き込みと読み取り、および書き込みと書き込みのデータ競合を検出できます。または、何か不足していますか?
3 に答える
または、何か不足していますか?
あなたはたくさん欠けています。Pubby が言ったように、T1 で読み取りを見てから書き込み、後で T2 で読み取りを行ってから書き込みを行う場合、人種の不在については何も言えません。関連するロックについて知っておく必要があります。
代わりに、Google のThreadSanitizerなどのツールを使用することをお勧めします。
アップデート:
しかし、私のアプローチはすべての人種をカバーするのでしょうか、それとも少なくとも一部の人種をカバーするのでしょうか?
ここおよび他の回答に対するあなたのコメントは、あなたが人種とは何かを理解していないことを示しているようです。
はい、あなたのアプローチはいくつかの人種を明らかにするかもしれません。それらのほとんどをカバーしないことが保証されています(これにより、演習が無駄になります)。
ウィキペディアの簡単な例を少し変更したものを次に示します。
簡単な例として、2つのスレッドT1とT2がそれぞれ、グローバル整数の値について算術を実行したいと仮定します。
- 整数 i = 0; (メモリー)
- T1 は i の値をメモリから register1 に読み込みます: 0
- T1 はレジスタ 1 の i の値をインクリメントします: (レジスタ 1 の内容) + 1 = 1
- T1 は register1 の値をメモリに格納します: 1
- T2 は i の値をメモリから register2 に読み込みます: 1
- T2 はレジスタ 2 の i の値を乗算します: (レジスタ 2 の内容) * 2 = 2
- T2 はレジスター 2 の値をメモリーに保管します: 2
- 整数 i = 2; (メモリー)
上記の場合、i の最終値は予想どおり 2 です。ただし、ロックや同期を行わずに 2 つのスレッドを同時に実行すると、操作の結果が正しくない可能性があります。以下の代替操作シーケンスは、このシナリオを示しています。
- 整数 i = 0; (メモリー)
- T1 は i の値をメモリから register1 に読み込みます: 0
- T2 は i の値をメモリからレジスタ 2 に読み込みます: 0
- T1 はレジスタ 1 の i の値をインクリメントします: (レジスタ 1 の内容) + 1 = 1
- T2 はレジスタ 2 の i の値を乗算します: (レジスタ 2 の内容) * 2 = 0
- T1 は register1 の値をメモリに格納します: 1
- T2 は register2 の値をメモリに保存します: 0
- 整数 i = 0; (メモリー)
i の最終値は、予想される結果の 2 ではなく 0 です。これは、2 番目のケースのインクリメント操作が相互に排他的ではないために発生します。相互に排他的な操作とは、メモリ ロケーションなどのリソースにアクセスしているときに中断できない操作です。最初のケースでは、T1 は変数 i へのアクセス中に中断されなかったため、その操作は相互に排他的でした。
これらの操作はすべてアトミックです。この特定の順序が最初の順序と同じセマンティクスを持たないため、競合状態が発生します。セマンティクスが最初のものと同じではないことをどのように証明しますか? この場合、それらが異なることはわかっていますが、競合状態がないことを確認するには、考えられるすべての順序を証明する必要があります。これを行うのは非常に困難であり、非常に複雑なため (おそらく NP 困難または AI 完全を必要とする)、確実にチェックすることはできません。
特定の注文が停止しない場合はどうなりますか? そもそもそれが決して止まらないことをどうやって知っていますか?基本的に、不可能な作業である停止問題を解決する必要があります。
競合を判断するために連続した読み取りまたは書き込みを使用することについて話している場合は、次のことに注意してください。
- 整数 i = 0; (メモリー)
- T2 は i の値をメモリからレジスタ 2 に読み込みます: 0
- T2 はレジスタ 2 の i の値を乗算します: (レジスタ 2 の内容) * 2 = 0
- T2 は register2 の値をメモリに保存します: 0
- T1 は i の値をメモリから register1 に読み込みます: 0
- T1 はレジスタ 1 の i の値をインクリメントします: (レジスタ 1 の内容) + 1 = 1
- T1 は register1 の値をメモリに格納します: 1
- 整数 i = 1; (メモリー)
これは最初のものと同じ読み取り/保存パターンを持ちますが、異なる結果になります。
最も明白なことは、複数のスレッドが同じメモリを使用していることです。それ自体は必ずしも悪いことではありません。
適切な用途には、セマフォによる保護、アトミック アクセス、RCU やダブル バッファリングなどのメカニズムが含まれます。
不適切な使用には、競合状態、true および false の共有が含まれます。
- 競合状態は主に順序の問題から生じます。特定のタスク A が実行の最後に何かを書き込み、タスク B が開始時にその値を必要とする場合、B の読み取りが A の完了後にのみ行われるようにすることをお勧めします。セマフォ、シグナルなどは、これに対する良い解決策です。または、もちろん同じスレッドで実行します。
- 真の共有とは、2 つ以上のコアが同じメモリ アドレスを積極的に読み書きしていることを意味します。これにより、他のコアのキャッシュ (およびもちろんメモリ) に変更を常に送信する必要があるため、プロセッサの速度が低下します。あなたのアプローチはこれを捉えることができますが、おそらくそれを強調することはできません。
- 偽の共有は、真の共有よりもさらに複雑です。プロセッサ キャッシュは、1 バイトではなく、複数の値を保持する「キャッシュ ライン」で機能します。コア A がラインのバイト 0 を叩き続け、コア B がバイト 4 に書き込み続ける場合、キャッシュの更新はプロセッサ全体を停止させます。