web-dev-qa-db-ja.com

スレッド制御の揮発性ブール値は間違っていると見なされますか?

この質問 に対する私の答えの結果として、私はキーワードvolatileとそれに関するコンセンサスについて読み始めました。私はそれについて多くの情報があるのを見る。古いものは今は間違っているように見え、新しいものはマルチスレッドプログラミングにはほとんど場所がないと言っている。したがって、特定の使用法を明確にしたいと思います(SOで正確な答えを見つけることができませんでした)。

また、マルチスレッドコードを作成するための一般的な要件と、volatileが問題を解決しない理由を理解していることも指摘しておきます。それでも、作業しているコードベースのスレッド制御にvolatileを使用するコードが表示されます。さらに、他のすべての共有リソースが適切に同期されているため、volatileキーワードを使用するのはこれだけです。

次のようなクラスがあるとします。

class SomeWorker
{
public:
    SomeWorker() : isRunning_(false) {}
    void start() { isRunning_ = true; /* spawns thread and calls run */ }
    void stop() { isRunning_ = false; }

private:
    void run()
    {
        while (isRunning_)
        {
            // do something
        }
    }
    volatile bool isRunning_;
};

簡単にするためにいくつかのことは省略されていますが、重要なことは、新しく生成されたスレッドで(volatile)ブール値をチェックして停止する必要があるかどうかを確認するオブジェクトを作成することです。このブール値は、ワーカーを停止させたいときに別のスレッドから設定されます。

私の理解では、この特定のケースでvolatileを使用する理由は、ループのレジスタにキャッシュする最適化を回避するためです。したがって、無限ループが発生します。ワーカースレッドは最終的に新しい値を取得するため、適切に同期する必要はありませんか?

これが完全に間違っていると考えられているかどうか、そして正しいアプローチが同期変数を使用することであるかどうかを理解したいですか?コンパイラ/アーキテクチャに違いはありますか?/cores?多分それは避ける価値のあるただのずさんなアプローチですか?

誰かがこれを明確にしてくれたら嬉しいです。ありがとう!

[〜#〜]編集[〜#〜]

これをどのように解決するかを(コードで)見てみたいと思います。

29
murrekatt

volatileはそのような目的に使用できますただしこれは標準C++の拡張です Microsoftによる

Microsoft固有

揮発性として宣言されたオブジェクトは(...)

  • 揮発性オブジェクトへの書き込み(volatile書き込み)にはリリースセマンティクスがあります。 (...)
  • 揮発性オブジェクトの読み取り(揮発性読み取り)には、取得セマンティクスがあります。 (...)

これにより、揮発性オブジェクトをマルチスレッドアプリケーションのメモリロックおよび解放に使用できます。(強調追加)

つまり、私が理解している限り、Visual C++コンパイラを使用する場合、volatile boolは、ほとんどの実用的な目的では atomic<bool> です。

newer VSバージョンはこの動作を制御する / volatile switch を追加するため、これは/volatile:msがアクティブな場合にのみ当てはまることに注意してください。

9
Martin Ba

synchronized変数は必要ありませんが、atomic変数は必要です。幸い、std::atomic<bool>を使用できます。

重要な問題は、複数のスレッドが同時に同じメモリにアクセスする場合、アクセスがアトミックでない限り、プログラム全体が明確に定義された状態でなくなることです。おそらく、ブール値は幸運であり、いずれにせよアトミックに更新される可能性がありますが、それが正しく行われていることを攻撃的に確信する唯一の方法は、アトミック変数を使用することです。

「作業中のコードベースを確認する」ことは、並行プログラミングの学習に関しては、おそらくあまり良い方法ではありません。並行プログラミングは非常に難しく、それを完全に理解している人はほとんどいません。自作コードの大部分(つまり、全体で専用の並行ライブラリを使用していない)が何らかの形で正しくないことは間違いありません。問題は、これらのエラーを観察または再現するのが非常に難しいため、わからない可能性があることです。

編集:あなたはあなたの質問で言っていませんどのようにブール値が更新されているので、私は最悪の事態を想定しています。たとえば、更新操作全体をグローバルロックでラップする場合、もちろん、同時メモリアクセスはありません。

8
Kerrek SB

マルチスレッド時に直面している3つの主要な問題があります。

1)同期とスレッドセーフ。複数のスレッド間で共有される変数は、一度に複数のスレッドによって書き込まれないように保護し、非アトミック書き込み中に読み取られないようにする必要があります。オブジェクトの同期は、それ自体がアトミックであることが保証されている特別なセマフォ/ミューテックスオブジェクトを介してのみ実行できます。 volatileキーワードは役に立ちません。

2)指示配管。 CPUは、コードの実行を高速化するために、一部の命令が実行される順序を変更できます。 CPUごとに1つのスレッドが実行されるマルチCPU環境では、CPUは、システム内の別のCPUが同じことを実行していることを知らなくても命令をパイプします。命令配管に対する保護は、メモリバリアと呼ばれます。それはすべて ウィキペディア でよく説明されています。メモリバリアは、専用のメモリバリアオブジェクトまたはシステム内のセマフォ/ミューテックスオブジェクトのいずれかを介して実装できます。コンパイラは、volatileキーワードが使用されている場合、コードでメモリバリアを呼び出すことを選択する可能性がありますが、それはかなり特別な例外であり、標準ではありません。コンパイラのマニュアルで検証せずに、volatileキーワードがこれを行ったとは思いません。

3)コンパイラがコールバック関数を認識しない。ハードウェア割り込みの場合と同様に、一部のコンパイラは、コード実行の途中でコールバック関数が実行され、値が更新されたことを認識しない場合があります。あなたはこのようなコードを持つことができます:

// main
x=true;
while(something) 
{   
  if(x==true)   
  {
    do_something();
  }
  else
  {
    do_seomthing_else();
    /* The code may never go here: the compiler doesn't realize that x 
       was changed by the callback. Or worse, the compiler's optimizer 
       could decide to entirely remove this section from the program, as
       it thinks that x could never be false when the program comes here. */
  } 
}

// thread callback function:
void thread (void)
{
  x=false;
}

この問題は、オプティマイザの設定に応じて、一部のコンパイラでのみ発生することに注意してください。この特定の問題は、volatileキーワードによって解決されます。


したがって、質問に対する答えは次のとおりです。マルチスレッドプログラムでは、volatileキーワードはスレッドの同期/安全性に役立ちません。メモリバリアとして機能しない可能性がありますが、コンパイラのオプティマイザによる危険な仮定を防ぐことができます。

7
Lundin

volatileを使用するだけで、すべてのスレッドが同じキャッシュを使用するシングルコアでのみ十分です。マルチコアでは、あるコアでstop()が呼び出され、別のコアでrun()が実行されている場合、CPUキャッシュの同期に時間がかかることがあります。つまり、2つのコアで2つが認識される可能性があります。 _isRunning__のさまざまなビュー。これは、run()が停止した後しばらくの間実行されることを意味します。

同期メカニズムを使用すると、プログラムをしばらく停止するという代償を払って、すべてのキャッシュが同じ値を取得するようになります。パフォーマンスと正確さのどちらが重要かは、実際のニーズによって異なります。

6
eran

これはあなたのケースでは機能しますが、クリティカルセクションを保護するためにこのアプローチは間違っています。それが正しければ、ミューテックスが使用されるほとんどすべての場合に揮発性ブールを使用できます。その理由は、揮発性変数がメモリバリアやキャッシュコヒーレンスメカニズムの適用を保証しないためです。それどころか、ミューテックスはそうします。言い換えると、ミューテックスがロックされると、すべてのコア間の一貫性を維持するために、キャッシュの無効化がすべてのコアにブロードキャストされます。揮発性の場合、これは当てはまりません。それにもかかわらず、Andrei Alexandrescu 非常に興味深いアプローチを提案しました 共有オブジェクトに同期を強制するためにvolatileを使用します。そして、あなたが見るように、彼はミューテックスでそれをします。 volatileは、同期せずにオブジェクトのインターフェースにアクセスするのを防ぐためにのみ使用されます。

0
PoP