スレッドプールとFork/Joinの究極の目標は同じです:両方とも、スループットを最大限に高めるために、使用可能なCPUパワーを最大限に活用したいと考えています。最大スループットとは、可能な限り多くのタスクを長期間に渡って完了することを意味します。それをするために何が必要ですか? (ハイパースレッディングの場合には、コアや仮想コアに「CPU」を同等に使用する)、CPU使用率を100%にするには十分です。
- 少なくてもスレッドを実行するとコアが使用されなくなるため、使用可能なCPUがある場合と同じくらい多くのスレッドを実行する必要があります。
- スレッド数を増やすとCPUを異なるスレッドに割り当てるスケジューラに負荷がかかり、CPU時間が計算機ではなくスケジューラに送られるため、使用可能なCPUの数と同じ数のスレッドを最大で実行する必要がありますタスク。
したがって、スループットを最大限にするには、CPUと同じ数のスレッドを用意する必要があることがわかりました。 Oracleのぼかしの例では、使用可能なCPUの数に等しい数のスレッドを持つ固定サイズのスレッドプールを使用することも、スレッドプールを使用することもできます。それは違いはありません、あなたは正しいです!
So when will you get into trouble with a thread pools? That is if a thread blocks, because your thread is waiting for another task to complete. Assume the following example:
class AbcAlgorithm implements Runnable {
public void run() {
Future aFuture = threadPool.submit(new ATask());
StepBResult bResult = stepB();
StepAResult aResult = aFuture.get();
stepC(aResult, bResult);
}
}
ここで見ているのは、A、B、Cの3つのステップからなるアルゴリズムです。AとBは互いに独立して実行できますが、ステップCにはステップAとBの結果が必要です。スレッドプールを作成し、タスクbを直接実行します。その後、スレッドはタスクAが完了するのを待ってステップCを続行します.AとBが同時に完了すると、すべて正常です。しかし、AがBよりも長くかかるとどうなるでしょうか?それは、タスクAの性質がそれを指示するためかもしれませんが、そうでないかもしれないので
スレッドAのスレッドは最初から利用可能であり、タスクAは待機する必要があります。 (使用可能なCPUが1つしかないため、スレッドプールにスレッドが1つしかない場合、デッドロックが発生することもありますが、今のところそれがポイントの外にあります)。ポイントは、タスクB を実行したスレッドがスレッド全体をブロックすることです。 CPUと同じ数のスレッドがあり、1つのスレッドがブロックされているので、 1つのCPUがアイドル状態です。
フォーク/結合はこの問題を解決します。フォーク/結合フレームワークでは、次のような同じアルゴリズムを記述します。
class AbcAlgorithm implements Runnable {
public void run() {
ATask aTask = new ATask());
aTask.fork();
StepBResult bResult = stepB();
StepAResult aResult = aTask.join();
stepC(aResult, bResult);
}
}
同じように見えますが、そうではありませんか?しかし、 aTask.join
はブロックされませんというヒントです。代わりにここには仕事を盗むというものがあります。スレッドは、過去にフォークされた他のタスクを見回し、それらで続行します。まず、フォークしたタスクが処理を開始したかどうかをチェックします。したがって、Aがまだ別のスレッドによって起動されていない場合、Aは次にAを実行し、そうでなければ他のスレッドのキューをチェックして作業を盗みます。別のスレッドのこの他のタスクが完了すると、Aが完了したかどうかをチェックします。上記のアルゴリズムであれば stepC
を呼び出すことができます。さもなければ、それは盗む別のタスクを探します。したがって、フォーク/ジョインプールは、ブロック動作にもかかわらず100%CPU使用率を達成することができます。
しかし、トラップがあります:Work-stealingは、 ForkJoinTask
の join
呼び出しに対してのみ可能です。別のスレッドを待ったり、I/Oアクションを待ったりするような外部のブロックアクションでは、実行できません。ですから、I/Oが完了するのを待つことは共通の課題ですか?この場合、Fork/Joinプールに追加のスレッドを追加できれば、ブロックアクションが完了すると直ちに停止します。これは2番目に良いことです。 ManagedBlocker
を使用している場合、 ForkJoinPool
は実際にそれを行うことができます。
フィボナッチ
In the JavaDoc for RecursiveTask is an example for calculating フィボナッチ numbers using Fork/Join. For a classic recursive solution see:
public static int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
As is explained int the JavaDocs this is a pretty dump way to calculate フィボナッチ numbers, as this algorithm has O(2^n) complexity while simpler ways are possible. However this algorithm is very simple and easy to understand, so we stick with it. Let's assume we want to speed this up with Fork/Join. A naive implementation would look like this:
class フィボナッチ extends RecursiveTask {
private final long n;
フィボナッチ(long n) {
this.n = n;
}
public Long compute() {
if (n <= 1) {
return n;
}
フィボナッチ f1 = new フィボナッチ(n - 1);
f1.fork();
フィボナッチ f2 = new フィボナッチ(n - 2);
return f2.compute() + f1.join();
}
}
このTaskが分割されているステップは短すぎるため、恐ろしく実行されますが、フレームワークが一般的にどのように機能するかを見ることができます:2つのsummandは独立して計算できますが、結果。したがって、半分は他のスレッドで行われます。デッドロックを起こすことなくスレッドプールで同じことをやってみましょう(可能ですが、それほど単純ではありません)。
Just for completeness: If you'd actually want to calculate フィボナッチ numbers using this recursive approach here is an optimized version:
class フィボナッチBigSubtasks extends RecursiveTask {
private final long n;
フィボナッチBigSubtasks(long n) {
this.n = n;
}
public Long compute() {
return fib(n);
}
private long fib(long n) {
if (n <= 1) {
return 1;
}
if (n > 10 && getSurplusQueuedTaskCount() < 2) {
final フィボナッチBigSubtasks f1 = new フィボナッチBigSubtasks(n - 1);
final フィボナッチBigSubtasks f2 = new フィボナッチBigSubtasks(n - 2);
f1.fork();
return f2.compute() + f1.join();
} else {
return fib(n - 1) + fib(n - 2);
}
}
}
This keeps the subtasks much smaller because they are only split when n > 10 && getSurplusQueuedTaskCount() < 2
is true, which means that there are significantly more than 100 method calls to do (n > 10
) and there are not very man tasks already waiting (getSurplusQueuedTaskCount() < 2
).
私のコンピュータ(4コア(ハイパースレッディングをカウントすると8)、Intel(R)Core(TM)i7-2720QM CPU @ 2.20GHz)では、古典的なアプローチで fib(50)
理論的には可能ではありませんが、フォーク/ジョインのアプローチではわずか18秒です。
概要
- はい、あなたの例では、Fork/Joinは古典的なスレッドプールよりも利点がありません。
- フォーク/結合は、ブロックが関係しているときにパフォーマンスを大幅に向上させることができます。
- フォーク/結合がデッドロックの問題を回避する