スレッドの作成に費用がかかると言われているのはなぜですか?


180

Javaチュートリアルによると、スレッドの作成にはコストがかかります。しかし、なぜそれが正確に高いのですか?Javaスレッドが作成され、その作成に費用がかかると、正確にはどうなりますか?私はこのステートメントを真実と見なしていますが、JVMでのスレッド作成のメカニズムに興味があります。

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

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


あなたが読んだチュートリアルがこれを言う文脈を知りません:彼らは作成自体が高価であること、または「スレッドの作成」が高価であることを意味しますか?私が見ようとする違いは、スレッドを作成する純粋なアクション(それをインスタンス化するなどと呼びます)と、スレッドを持っているという事実(スレッドの使用:明らかにオーバーヘッドがある)の間です。どちらが主張されていますか?//どちらについて質問しますか?
ナンヌ2011年

9
@typoknig-新しいスレッドを作成しないことに比べて高価です:)
willcodejavaforfood


1
勝利のためのスレッドプール。タスクの新しいスレッドを常に作成する必要はありません。
Alexander Mills

回答:


149

関与する作業がかなりあるため、Javaスレッドの作成はコストがかかります。

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

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

これらすべてのコストはプラットフォーム固有ですが、これまでに遭遇したどのJavaプラットフォームでも安価ではありません。


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

Peter Lawreyのベンチマークは、最近ではスレッドの作成が絶対的にかなり高速であることを示していますが、これがJavaやOSの改善によるものか、それともプロセッサの速度が速いかは不明です。しかし、彼の数字はまだあなたが作成/新しいスレッドを毎回開始対スレッドプールを使用する場合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 mmapMAP_ANONYMOUSフラグはメモリをゼロに初期化します。

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


2
@Raedwald-高価なのは初期化部分です。どこかで、何か(例えば、GCやOS)は、ブロックがスレッドスタックに変換される前にバイトをゼロにします。一般的なハードウェアでは、物理メモリサイクルがかかります。
スティーブンC

2
「どこかで、何か(例えば、GCやOS)がバイトをゼロにするでしょう」。そうなる?セキュリティ上の理由から、新しいメモリページの割り当てが必要な場合、OSはそれを行います。しかし、それは珍しいことです。そして、OSはすでにゼロにされたページのキャッシュを保持するかもしれません(IIRC、Linuxはそうします)。JVMがJavaプログラムがそのコンテンツを読み取るのを妨げるとすれば、なぜGCは煩わしいのでしょうか?malloc()JVMがよく使用する可能性のある標準C 関数は、割り当てられたメモリがゼロにされることを保証しないことに注意してください(おそらく、このようなパフォーマンスの問題だけを回避するため)。
Raedwald、2011

1
stackoverflow.com/questions/2117072/…は、「1つの主要な要因は各スレッドに割り当てられたスタックメモリである」と同意しています。
Raedwald、2011

2
@Raedwald-スタックが実際に割り当てられる方法については、更新された回答を参照してください。
スティーブンC

2
それはによって割り当てられたメモリページこと(可能性さえも)可能であるmmap()コールは、コピー・オン・ライトされているゼロページにマッピングされ、そのinitailisationが内に入っていないが起こるのでmmap()、それ自体が、ページが最初にされたときに書かれた時には、その後、1ページのみ時間。つまり、スレッドが実行を開始すると、作成者スレッドではなく作成されたスレッドがコストを負担します。
Raedwald

76

他の人たちは、スレッドのコストがどこから来るのかについて議論しました。スレッドの作成は、多くの操作に比べて高価な、しかし、それはないなぜこの回答カバーは比較的高価であるタスク実行の選択肢に比べて比較的安価。

別のスレッドでタスクを実行する最も明白な方法は、同じスレッドでタスクを実行することです。これは、より多くのスレッドが常に優れていると想定している人にとって理解するのが困難です。ロジックは、タスクを別のスレッドに追加するオーバーヘッドが節約した時間よりも大きい場合、現在のスレッドでタスクを実行する方が速くなる可能性があるということです。

もう1つの方法は、スレッドプールを使用することです。スレッドプールは、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です。これは、ほとんどではないにしても、多くのユースケースで取るに足らないことと考えられます。相対的に言えば、これは他の方法よりも高価であり、状況によっては、スレッドプールを使用するか、スレッドをまったく使用しない方がより良いソリューションです。


8
これはすばらしいコードです。簡潔に言えば、要点を明確にし、その趣旨を明確に示しています。
ニコラス

最後のブロックでは、最初の2つのブロックでメインスレッドがワーカースレッドの書き込みと並行して削除されるため、結果は歪んでいると思います。ただし、最後のブロックでは、取るアクションはすべて順次実行されるため、値が拡張されます。スレッドが完了するのを待つために、おそらくqueue.clear()を使用し、代わりにCountDownLatchを使用できます。
Victor Grazi 2013

@VictorGrazi結果を一元的に収集したいと思います。どちらの場合も同じ量のキューイング作業を行っています。カウントダウンラッチは少し速くなります。
Peter Lawrey 2013

実際、カウンターをインクリメントするなど、常に高速な処理を行わないのはなぜですか。BlockingQueue全体を削除します。最後にカウンターをチェックして、コンパイラーがインクリメント操作を最適化しないようにします
Victor Grazi

この場合、@ graziでそれを行うことができますが、カウンターで待機することは非効率的である可能性があるため、ほとんどの場合現実的ではありません。そうした場合、例の違いはさらに大きくなります。
Peter Lawrey、2013

31

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

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


9
Btw kb =キロビット、kB =キロバイト。GB =ギガビット、GB =ギガバイト。
Peter Lawrey、2013

@PeterLawreyは、「kb」と「kB」の「k」を大文字にするので、「Gb」と「GB」に対称性がありますか?これらは私を悩ませます。
ジャック

3
@ジャックa K= 1024およびk= 1000があります。;)en.wikipedia.org/wiki/Kibibyte
Peter Lawrey

9

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

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

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

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

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

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


19
「……解雇されて殺されない安定した労働者のカップル……」なぜ私は職場の状況について考え始めたのですか?:-)
スティーブンC

6

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


@Raedwald、個別のスタックを使用するjvmは何ですか?
bestss

1
フィリップJPは2スタックと言います。
Raedwald、2011年

私の知る限り、すべてのJVMはスレッドごとに2つのスタックを割り当てます。ガベージコレクションがJavaコードを(JITされている場合でも)フリーキャストとは異なる方法で処理することは有用ですc。
フィリップJF 2011

@フィリップJF詳しく説明していただけますか?Javaコード用とネイティブコード用の2つのスタックとはどういう意味ですか?それは何をするためのものか?
グラインダー

「私の知る限り、すべてのJVMはスレッドごとに2つのスタックを割り当てます。」-これを裏付ける証拠は見たことがありません。おそらく、JVM仕様のopstackの本質を誤解していると思います。(これはバイトコードの動作をモデル化する方法であり、実行時にそれらを実行するために使用する必要があるものではありません。)
Stephen C

1

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

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

それは、制御状態構造、つまり、実行可能、待機などの状態を設定する必要があります。

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.