web-dev-qa-db-ja.com

スレッドを作成するのに費用がかかるのはなぜですか?

Javaチュートリアルでは、スレッドの作成には費用がかかると述べています。しかし、なぜ正確に費用がかかるのですか?Java高価ですか?私は声明を真実だと思っていますが、私はJVMでのスレッド作成のメカニズムに興味があります。

スレッドのライフサイクルのオーバーヘッド。スレッドの作成と分解は無料ではありません。実際のオーバーヘッドはプラットフォームによって異なりますが、スレッドの作成には時間がかかり、要求処理に遅延が発生し、JVMおよびOSによる処理アクティビティが必要になります。ほとんどのサーバーアプリケーションのように、リクエストが頻繁で軽量な場合、リクエストごとに新しいスレッドを作成すると、大量のコンピューティングリソースが消費される可能性があります。

からJava並行性の実践
ブライアン・ゲッツ、ティム・パイエルス、ジョシュア・ブロック、ジョセフ・ボウビア、デビッド・ホームズ、ダグ・リー
ISBN-10を印刷:0-321-34960-1

167
kachanov

Javaスレッドの作成は、かなりの作業が伴うため、費用がかかります。

  • スレッドスタック用に大きなメモリブロックを割り当てて初期化する必要があります。
  • ホストOSでネイティブスレッドを作成/登録するには、システムコールを作成する必要があります。
  • 記述子を作成、初期化して、JVM内部データ構造に追加する必要があります。

また、スレッドは、リソースが有効である限りリソースを拘束するという意味で高価です。例えばスレッドスタック、スタックから到達可能なオブジェクト、JVMスレッド記述子、OSネイティブスレッド記述子。

これらすべてのコストはプラットフォーム固有ですが、私が今まで出会ったJavaプラットフォームでは安くはありません。


Google検索で 古いベンチマーク が見つかりました。これは、2002ヴィンテージLinuxを実行している2002ヴィンテージデュアルプロセッサXeonでのSun Java 1.4.1でのスレッド作成率が毎秒4,000を報告します。 。より現代的なプラットフォームはより良い数を与えます...そして、私は方法論についてコメントすることはできません...しかし、少なくともそれはどのくらい高価なスレッド作成がそうである可能性が高いのかを示します。

Peter Lawreyのベンチマークは、スレッド作成が最近では絶対的にかなり速いことを示していますが、Javaおよび/またはOSの改善によるものであるか、またはより速いプロセッサ速度であるかは不明です。しかし、彼の数字stillは、毎回新しいスレッドを作成/開始するよりもスレッドプールを使用する場合、150倍以上の改善を示しています。 (そして、彼はこれがすべて相対的であると主張しています...)


(上記は「グリーンスレッド」ではなく「ネイティブスレッド」を前提としていますが、最新のJVMはすべてパフォーマンス上の理由からネイティブスレッドを使用します。グリーンスレッドの作成はおそらく安価ですが、他の分野で支払います。)


Javaスレッドのスタックが実際にどのように割り当てられるかを調べるために、少し掘り下げました。 Linux上のOpenJDK 6の場合、スレッドスタックは、ネイティブスレッドを作成するpthread_createの呼び出しによって割り当てられます。 (JVMはpthread_createに事前に割り当てられたスタックを渡しません。)

次に、pthread_create内で、スタックはmmapの呼び出しによって次のように割り当てられます。

mmap(0, attr.__stacksize, 
     PROT_READ|PROT_WRITE|PROT_EXEC, 
     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)

man mmapによれば、MAP_ANONYMOUSフラグにより​​、メモリがゼロに初期化されます。

したがって、新しいJavaスレッドスタックが(JVM仕様に従って)ゼロ化されることは必須ではないかもしれませんが、実際には(少なくともLinux上のOpenJDK 6では)ゼロ化されます。

142
Stephen C

他の人は、スレッドのコストがどこから来るのかを議論しました。この答えは、スレッドの作成が多くの操作に比べてそれほど高価ではない理由をカバーしていますが、比較的タスク実行の代替手段である比較的より安価です。

別のスレッドでタスクを実行する最も明白な代替方法は、同じスレッドでタスクを実行することです。これは、スレッドが多ければ多いほど良いと考える人にとっては理解するのが困難です。ロジックは、タスクを別のスレッドに追加するオーバーヘッドが保存時間よりも大きい場合、現在のスレッドでタスクを実行する方が速くなる可能性があるということです。

別の方法は、スレッドプールを使用することです。スレッドプールは、次の2つの理由により効率的です。 1)すでに作成されたスレッドを再利用します。 2)スレッド数を調整/制御して、最適なパフォーマンスを確保できます。

次のプログラムが印刷されます。

Time for a task to complete in a new Thread 71.3 us
Time for a task to complete in a thread pool 0.39 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 65.4 us
Time for a task to complete in a thread pool 0.37 us
Time for a task to complete in the same thread 0.08 us
Time for a task to complete in a new Thread 61.4 us
Time for a task to complete in a thread pool 0.38 us
Time for a task to complete in the same thread 0.08 us

これは、各スレッドオプションのオーバーヘッドを明らかにする簡単なタスクのテストです。 (このテストタスクは、現在のスレッドで実際に最適に実行されるタスクの一種です。)

final BlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
Runnable task = new Runnable() {
    @Override
    public void run() {
        queue.add(1);
    }
};

for (int t = 0; t < 3; t++) {
    {
        long start = System.nanoTime();
        int runs = 20000;
        for (int i = 0; i < runs; i++)
            new Thread(task).start();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a new Thread %.1f us%n", time / runs / 1000.0);
    }
    {
        int threads = Runtime.getRuntime().availableProcessors();
        ExecutorService es = Executors.newFixedThreadPool(threads);
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            es.execute(task);
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in a thread pool %.2f us%n", time / runs / 1000.0);
        es.shutdown();
    }
    {
        long start = System.nanoTime();
        int runs = 200000;
        for (int i = 0; i < runs; i++)
            task.run();
        for (int i = 0; i < runs; i++)
            queue.take();
        long time = System.nanoTime() - start;
        System.out.printf("Time for a task to complete in the same thread %.2f us%n", time / runs / 1000.0);
    }
}
}

ご覧のとおり、新しいスレッドを作成するのにかかる費用は約70 µsです。これは、ほとんどではないにしても、多くのユースケースで些細なことと見なすことができます。相対的に言えば、他の方法よりも高価であり、状況によっては、スレッドプールを使用するか、スレッドをまったく使用しない方が優れたソリューションです。

71
Peter Lawrey

理論的には、これはJVMに依存します。実際には、すべてのスレッドには比較的大量のスタックメモリがあります(デフォルトでは256 KB、と思います)。さらに、スレッドはOSスレッドとして実装されるため、スレッドの作成にはOS呼び出し、つまりコンテキストスイッチが含まれます。

コンピューティングの「高価」は常に非常に相対的であることを理解してください。スレッドの作成は、ほとんどのオブジェクトの作成に比べて非常に高価ですが、ランダムなハードディスクのシークに比べてそれほど高価ではありません。すべてのコストでスレッドを作成することを避ける必要はありませんが、毎秒数百のスレッドを作成するのは賢明なことではありません。ほとんどの場合、設計で多数のスレッドが必要な場合は、サイズが制限されたスレッドプールを使用する必要があります。

29

スレッドには次の2種類があります。

  1. 適切なスレッド:これらは、基盤となるオペレーティングシステムのスレッド機能に関する抽象化です。したがって、スレッドの作成はシステムと同じくらい高価です。常にオーバーヘッドがあります。

  2. 「グリーン」スレッド:JVMによって作成およびスケジュールされます。これらは安価ですが、適切な並列処理は行われません。これらはスレッドのように動作しますが、OSのJVMスレッド内で実行されます。私の知る限り、これらはあまり使用されません。

スレッド作成のオーバーヘッドで考えられる最大の要因は、stack-sizeあなたがスレッドに定義したことです。スレッドのスタックサイズは、VMの実行時にパラメーターとして渡すことができます。

それ以外は、スレッドの作成は主にOSに依存し、VMの実装にも依存します。

スレッドを作成するのは、起動を計画している場合は高価ですランタイムの毎秒2000スレッド。JVMはそれを処理するように設計されていません。何度も解雇されたり殺されたりしない安定した労働者が2人いる場合は、リラックスしてください。

8
slezica

Threadsを作成するには、1つではなく2つの新しいスタックを作成する必要があるため、かなりの量のメモリを割り当てる必要があります(1つはJavaコード、1つはネイティブコード)。 Executors /Thread Poolsは、 Executor の複数のタスクにスレッドを再利用することにより、オーバーヘッドを回避できます。

6
Philip JF

明らかに、問題の核心は「高価な」という意味です。

スレッドは、スタックを作成し、runメソッドに基づいてスタックを初期化する必要があります。

制御ステータス構造、つまり、実行可能な状態、待機中などの状態を設定する必要があります。

これらの設定には、おそらくかなりの同期があります。

0
MeBigFatGuy