8.競合条件と臨界セグメント

3475 ワード

競合条件は、臨界部分の内部で発生する可能性のある特殊な条件である.臨界部分は、マルチスレッドによって実行されているコードであり、スレッドの実行順序が臨界部分の同時実行の結果に影響を及ぼす.
マルチスレッドが1つの臨界セグメントを実行する結果がスレッドに依存して実行される順序が異なる場合があり、この臨界セグメントは競合条件を含む.この競合条件の語条は,このスレッドがこの臨界セグメントを通過する暗喩を競合していることに由来し,この競合の結果はこの臨界セグメントを実行する結果に影響を及ぼす.
これは少し複雑に聞こえるかもしれませんが、競合条件と臨界セグメントについて以下の部分で詳しく説明します.
クリティカルセグメント
同じアプリケーション内で1つ以上のスレッドを実行しても、彼自身に問題は発生しません.複数のスレッドが同じリソースにアクセスすると問題が発生します.たとえば、同じメモリ(変数、配列、オブジェクト)、システム(データベース、ウェブサービス)、ファイルなどです.
実際には、1つ以上のスレッドがこれらのリソースを書くと問題が発生します.リソースが変更されない限り、複数のスレッドに同じリソースを読み込むのは安全です.
ここでは、複数のスレッドの同僚が実行に失敗する可能性がある例を示します.
 public class Counter {

     protected long count = 0;

     public void add(long value){
         this.count = this.count + value;
     }
  }

スレッドAとBが同じCounterクラスのインスタンスを実行している場合のaddメソッドを想像します.オペレーティングシステムがスレッド間でいつ切り替わるかは、ここではわかりません.addメソッドのコードはjava仮想マシンによって個別の原子命令として実行されません.これと同様に、一連のより小さな命令セットとして実行されます.
  • メモリからthisを読み出す.count値はレジスタに入ります.
  • value値をレジスタに追加します.
  • レジスタの値をメモリに書き込みます.

  • スレッドAとBの混合実行で何が起こるかを観察します.
           this.count = 0;
    
       A:  Reads this.count into a register (0)
       B:  Reads this.count into a register (0)
       B:  Adds value 2 to register
       B:  Writes register value (2) back to memory. this.count now equals 2
       A:  Adds value 3 to register
       A:  Writes register value (3) back to memory. this.count now equals 3

    この2つのスレッドはcounterに2と3を追加したいです.したがって、この2つのスレッドの実行後の値は5である必要があります.しかしながら、この2つのスレッドは実行時に交差するため、結果は異なるもので終了する.
    上記の実行順序の例では、両方のスレッドがメモリから0という値に読み出されます.そして、それぞれの値を追加し、2と3をその値に追加し、この結果をメモリに書きます.代わりに5countに残された値は、最後のスレッドが彼に書いた値になります.上記の例ではスレッドAであるが、スレッドBである可能性もある.
    臨界セグメントにおける競合条件
    上記の例のaddメソッドのコードには臨界セグメントが含まれている.複数のスレッドがこの臨界セグメントを実行すると,競合条件が発生する.
    より正式には、2つのスレッドのこのような状況は同じリソースを競合しており、リソースがアクセスされる順序が重要であり、競合条件と呼ばれています.1つのコード部分が競合条件をもたらすことを臨界セグメントと呼ぶ.
    競合防止条件
    競合条件の発生を防止するためには,実行される臨界セグメントが原子命令として実行されることを確保しなければならない.それは、単一のスレッドが実行されると、最初のスレッドがこの臨界セグメントを離れるまで他のスレッドが実行できないことを意味します.
    競合条件は,臨界セグメントにおいてスレッド同期を用いることによって回避できる.スレッド同期は、Javaコードの同期ロックを使用して取得できます.スレッド同期は、ロックやjavaのような他の同期概念を用いて取得することもできる.util.concurrent.atomic.AtomicIntegerの原子変数.
    臨界セグメントのスループット
    より小さな臨界セグメントの場合、臨界セグメント全体の同期ロックが動作する可能性がある.しかし、より大きな臨界セグメントについては、それをより小さな臨界セグメントに分解することがより意味があり、マルチスレッドがより小さな臨界セグメントを実行することを可能にする.全体的に共有リソースの競合を低減し、臨界セグメント全体のスループットを増加させる可能性があります.
    ここには非常に簡単なJavaの例があります.
    public class TwoSums {
        
        private int sum1 = 0;
        private int sum2 = 0;
        
        public void add(int val1, int val2){
            synchronized(this){
                this.sum1 += val1;   
                this.sum2 += val2;
            }
        }
    }

    このaddメソッドがこの2つのsum変数に価値をどのように追加するかに注意してください.競合条件を予防するために,内部で実行される和を求めるJava同期ロックがある.この実装に伴い,この和を実行できるスレッドは1つしかない.
    しかし、この2つのsum変数は互いに独立しているため、2つの分離された同期ロックに分離することができます.
    public class TwoSums {
        
        private int sum1 = 0;
        private int sum2 = 0;
        
        public void add(int val1, int val2){
            synchronized(this){
                this.sum1 += val1;   
            }
            synchronized(this){
                this.sum2 += val2;
            }
        }
    }

    2つのスレッドは、このaddメソッドを同時に実行することができることに注意してください.1つのスレッドは1つ目の同期ロックを取得し、もう1つのスレッドは2つ目の同期ロックを取得します.このようにして、スレッド間で待つ時間が少なくなります.
    もちろん、この例はとても簡単です.本当の生活では,臨界セグメント分離の共有リソースはより複雑になり,より多くの実行順序可能性の解析が必要になる可能性がある.
    翻訳先:http://tutorials.jenkov.com/java-concurrency/race-conditions-and-critical-sections.html