multithreadingマルチスレッドの開始


備考

マルチスレッドは、タスクを別々の実行スレッドに分割するプログラミング手法です。これらのスレッドは、異なる処理コアに割り当てられるか、タイムスライスによって同時に実行されます。

マルチスレッドプログラムを設計するときは、可能な限り独立してスレッドを作成して、最大のスピードアップを達成する必要があります。
実際には、スレッドは完全に独立していることはほとんどなく、同期が必要になります。
最大理論上のスピードアップは、 アムダールの法則を用いて計算することができる。

利点

  • 利用可能な処理リソースを効率的に使用して実行時間を短縮
  • 時間のかかる計算や高価なI / O操作を分割する必要がなく、プロセスの応答性を維持する
  • 他のオペレーションよりも簡単に特定のオペレーションの優先順位付け

短所

  • 慎重な設計がなければ、見つけにくいバグが導入される可能性があります
  • スレッドを作成するにはオーバーヘッドが必要です

同じスレッドを2回実行できますか?

同じスレッドを2回実行できることが最も頻繁に疑問でした。

これに対する答えは、1つのスレッドが1回だけ実行できることです。

同じスレッドを2回実行しようとすると、最初に実行されますが、2回目にエラーが発生し、エラーはIllegalThreadStateExceptionになります。

public class TestThreadTwice1 extends Thread{  
 public void run(){  
   System.out.println("running...");  
 }  
 public static void main(String args[]){  
  TestThreadTwice1 t1=new TestThreadTwice1();  
  t1.start();  
  t1.start();  
 }  
}  
 

出力

running
       Exception in thread "main" java.lang.IllegalThreadStateException
 

デッドロック

デッドロックは、2つ以上のスレッドからなるグループのすべてのメンバが、他のメンバの1つが処理を続ける前に何か(例えば、ロックを解除する)を待つときに発生します。介入がなければ、スレッドは永遠に待機します。

デッドロックが発生しやすい設計の擬似コードの例は次のとおりです。

thread_1 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

thread_2 {
    acquire(B)
    ...
    acquire(A)
    ...
    release(A, B)
}
 

デッドロックが発生する可能性がありますthread_1 取得したA 、しかし、まだB 、及びthread_2 取得したB 、ではなく。 A 次の図に示すように、両方のスレッドは永遠に待機します。 デッドロックダイアグラム

デッドロックを回避する方法

経験則として、ロックの使用を最小限に抑え、ロックとロック解除の間のコードを最小限に抑えます。

同じ順序でロックを取得する

thread_2 の再設計はこの問題を解決します:

thread_2 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}
 

両方のスレッドは同じ順序でリソースを取得し、デッドロックを回避します。

このソリューションは、「リソース階層ソリューション」として知られています。それはダイクストラによって「ダイニング哲学者の問題」の解決策として提案されました。

場合によっては、ロック取得の厳密な順序を指定しても、そのような静的ロック取得順序を実行時に動的にすることができます。

次のコードを検討してください:

void doCriticalTask(Object A, Object B){
     acquire(A){
        acquire(B){
            
        }
    }
}
 

ここでロックの取得順序が安全であっても、thread_1がObject_1をパラメータA、Object_2をパラメータB、thread_2が逆の順序、つまりObject_2をパラメータA、Object_1をパラメータBとしてこのメ​​ソッドにアクセスすると、デッドロックが発生する可能性があります。

このような状況では、Object_1とObject_2の両方を使用していくつかの種類の計算、たとえば両方のオブジェクトのハッシュコードを使用して導出された独自の条件を持つ方が良いです。取得順序をロックする

Accountオブジェクトの場合はaccountNumberのようにいくつかの一意のキーを持ちます。

void doCriticalTask(Object A, Object B){
    int uniqueA = A.getAccntNumber();
    int uniqueB = B.getAccntNumber();
    if(uniqueA > uniqueB){
         acquire(B){
            acquire(A){
                
            }
        }
    }else {
         acquire(A){
            acquire(B){
                
            }
        }
    }
}
 

こんにちはマルチスレッド - 新しいスレッドの作成

この簡単な例は、Javaで複数のスレッドを開始する方法を示しています。スレッドが順番に実行されることは保証されておらず、実行の順序は実行ごとに異なる可能性があることに注意してください。

public class HelloMultithreading {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new MyRunnable(i));
            t.start();
        }
    }

    public static class MyRunnable implements Runnable {

        private int mThreadId;

        public MyRunnable(int pThreadId) {
            super();
            mThreadId = pThreadId;
        }

        @Override
        public void run() {
            System.out.println("Hello multithreading: thread " + mThreadId);
        }

    }

}
 

目的

スレッドは、コマンド処理が発生するコンピューティングシステムの低レベル部分です。 CPU / MCUハードウェアによってサポート/提供されます。ソフトウェア方法もあります。マルチスレッドの目的は、可能であれば、互いに並列に計算を実行することです。したがって、所望の結果を、より小さい時間スライスで得ることができる。

競争条件

データ競合または競合状態は、マルチスレッドプログラムが適切に同期されていない場合に発生する可能性がある問題です。 2つ以上のスレッドが同期なしで同じメモリにアクセスし、少なくとも1つのアクセスが「書き込み」操作である場合、データ競合が発生します。これにより、プラットフォームに依存し、プログラムの動作に一貫性がない可能性があります。たとえば、計算の結果はスレッドのスケジューリングに依存する可能性があります。

読者 - ライター問題

writer_thread {
    write_to(buffer)
}

reader_thread {
    read_from(buffer)
}
 

簡単な解決策:

writer_thread {
    lock(buffer)
    write_to(buffer)
    unlock(buffer)
}

reader_thread {
    lock(buffer)
    read_from(buffer)
    unlock(buffer)
}
 

この単純な解決策は、リーダースレッドが1つしかない場合はうまく機能しますが、複数のスレッドが存在する場合は、リーダースレッドが同時に読み取ることができるため、実行が不必要に遅くなります。

この問題を回避するソリューションは次のとおりです。

writer_thread {
    lock(reader_count)
    if(reader_count == 0) {
        write_to(buffer)
    }
    unlock(reader_count)
}

reader_thread {
    lock(reader_count)
    reader_count = reader_count + 1
    unlock(reader_count)

    read_from(buffer)

    lock(reader_count)
    reader_count = reader_count - 1
    unlock(reader_count)
}
 

reader_count は、書き込み操作が完了していない間に読み取りを開始できないように、書き込み操作全体を通してロックされています。

今では多くの読者が同時に読むことができますが、新しい問題が発生する可能性があります: reader_count は決して0 に達することができないため、ライタースレッドは決してバッファーに書き込むことができません。これは飢餓と呼ばれ、それを避けるためのさまざまな解決策があります。


正しいように見えるかもしれないプログラムでさえ問題になることがあります:

boolean_variable = false 

writer_thread {
    boolean_variable = true
}

reader_thread {
    while_not(boolean_variable)
    {
       do_something()
    }         
}
 

リーダスレッドはライタスレッドからの更新を決して見ることができないので、サンプルプログラムは決して終了しないかもしれません。たとえば、ハードウェアがCPUキャッシュを使用している場合は、値がキャッシュされる可能性があります。また、通常のフィールドへの書き込みまたは読み取りはキャッシュのリフレッシュにつながりませんので、変更された値は読み取りスレッドによって決して見えないことがあります。

C ++とJavaは、いわゆるメモリモデルにおいて、適切に同期化されていることを、 C ++メモリモデルJavaメモリモデルと定義しています。

Javaでは、フィールドをvolatileとして宣言することができます。

volatile boolean boolean_field;
 

C ++では、フィールドをアトミ​​ックとして宣言することができます:

std::atomic<bool> data_ready(false)
 

データ競争は競合状態の一種です。しかし、すべての競合条件がデータ競争であるとは限りません。 2つ以上のスレッドによって呼び出された以下は、競合状態につながりますが、データ競合にはつながりません。

class Counter {
    private volatile int count = 0;

    public void addOne() {
     i++;
    }
}
 

Java Memory Model仕様に従って正しく同期されるため、データ競合ではありません。しかし、それは競合状態につながります。例えば、結果はスレッドのインターリーブに依存します。

すべてのデータ競合がバグであるとは限りません。いわゆる良性競合状態の例は、sun.reflect.NativeMethodAccessorImplです。

class  NativeMethodAccessorImpl extends MethodAccessorImpl {
    private Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    
    NativeMethodAccessorImpl(Method method) {
        this.method = method;
    }

    public Object invoke(Object obj, Object[] args)
        throws IllegalArgumentException, InvocationTargetException
    {
        if (++numInvocations > ReflectionFactory.inflationThreshold()) {
              MethodAccessorImpl acc = (MethodAccessorImpl)
            new MethodAccessorGenerator().
            generateMethod(method.getDeclaringClass(),
                             method.getName(),
                             method.getParameterTypes(),
                             method.getReturnType(),
                             method.getExceptionTypes(),
                             method.getModifiers());
                             parent.setDelegate(acc);
          }
          return invoke0(method, obj, args);
    }
    ...
}
 

ここでは、numInvocationのカウントの正確さよりもコードのパフォーマンスが重要です。