Previous | Next | Trail Map | Writing Java Programs | スレッドの制御


スレッドの優先順位

前にこのレッスンで、スレッドが同時に実行することを断言した。 概念的にこれは真であるが、実際のところ通常はそうではない。 ほとんどのコンピュータ構成は単一CPUを持っているので、スレッドは実際には、同時処理をシミュレーションするような方法で一度に1つを実行する。 単一CPUでのマルチスレッドの実行は、スケジューリングと呼ばれる。 Java 実行時は、固定優先順位スケジューリングとして知られている、非常に単純な決定性スケジューリングアルゴリズムをサポートする。 このアルゴリズムは、他の 「実行可能な」 スレッドに関連する優先順位に基づいて、スレッドをスケジュールする。

Java スレッドが作成されると、作成したスレッドから優先順位を継承する。 setPriority() メソッドを使用してスレッドを作成した後は、いつでもスレッドの優先順位も変更することができる。 スレッドの優先順位は MIN_PRIORITY と MAX_PRIORITY の間を変動する(定数は Thread クラスで定義する)。 随時、マルチスレッドを実行する準備ができている時は、実行時システムは最も高い優先順位で「実行可能な」スレッドを選択し実行させる。 ただスレッドが停止したり、譲歩したり、なんらかの理由で「実行不可能になる」 時だけは、低い優先順位スレッドが実行し始める。 もし CPU を待っている同じ優先順位のスレッドが2つあると、スケジューラーはラウンドロビン方式で選択する。

Java 実行時システムのスレッドスケジューリングアルゴリズムもまた、先取り式である。 他のすべての「実行可能な」スレッドより高い優先順位を持つスレッドが「実行可能」になった場合はいつでも、実行システムは新規のより優先順位の高いスレッドを選択して実行する。 新規の優先順位の高いスレッドが他のスレッドに先制すると言われる。

Java 実行時システムのスレッドスケジューリングスキーマは、次の単純なルールで要約される。


規則:  随時、最も優先順位の高い実行可能なスレッドが実行している。


400,000 ミクロンのスレッドレース

この Java ソースコード は、異なる優先順位を持つ2つの「走者」スレッド間のレースを活気づけるアプレットを実装する。 アプレットの上でマウスをクリックすると、2人の走者がスタートする。 「 1 」のラベルを付けた先頭の走者は、 1 の優先順位( Java システムで可能な最低のスレッド優先順位)を持っている。 「 2 」のラベルを付けた2番目の走者は、 2 の優先順位を持っている。

試してみよう: 以下のアプレットをクリックしてレースを開始しよう。


ブラウザはアプレットタグを理解しない。 理解していれば、ここでレースアプレットのスクリーンショットが見られるだろう。


これが両方の runners run() メソッドである。

public int tick = 1;
public void run() {
    while (tick < 400000) {
        tick++;
    }
}
 

この run() メソッドは 1 から400,000まで数える。 インスタンス変数 tick は、public である。これは走者がどのくらい進んだか(その線の長さがどのくらいか)を判断するためにアプレットがこの値を使用するからである。

2つの走者スレッドに加えて、このアプレットは描画を処理する3番目のスレッドを持っている。 描画スレッドの run() メソッドは無限ループを含む。ループの各繰返しの間にそれぞれの走者のラインを描いて(その長さは走者のtick 変数から計算される)、次に 10 ミリ秒間スリープする。 描画スレッドは 3 のスレッド優先順位を持っている。これは、どの走者よりも高い。 これにより、描画スレッドが 10 ミリ秒後に呼び起こされる時はいつでも、それが最も優先順位の高いスレッドになり、現在実行中の走者スレッドに先んじてラインを描く。 このようにして、ラインがページをゆっくり横切っていくのを見ることができる。

見てわかるように、1人の走者が一方より高い優先順位を持っているので、これは公正なレースではない。 描画スレッドが、 10 ミリ秒間スリープすることで CPU を譲るたびに、スケジューラーは最も高い優先順位で実行可能なスレッドを選択して実行させる。このアプレットの場合、それは常に「 2 」のラベルを付けた走者である。 以下にもう1つの「公正なレース」を実装するバージョンのアプレットがある。つまり、両走者は同じ優先順位を持っていて、実行のため選択される等しいチャンスがある。

試してみよう: 再度、マウスをクリックしてレースを開始しよう。


あなたのブラウザはアプレットタグを理解しない。理解していれば、ここでレースアプレットのスクリーンショットが見られるだろう。


このレースでは、描画スレッドがスリープすることで CPU を譲る度に、 CPU を待っている等しい優先順位で実行可能な2つのスレッド− 走者 −がある。スケジューラーは実行するスレッドを1つ選ばなくてはならない。 この状況では、スケジューラーは次のスレッドをラウンドロビン方式で実行するように選択する。

利己的なスレッド

実際に上記のレースで使用した走者クラスは、「社会的に害された」スレッドの動作を実装する。上記のレースで使用した走者クラスから run() メソッドを再び呼び出す。

public int tick = 1;
public void run() {
    while (tick < 400000) {
        tick++;
    }
}
 

run() メソッドでの while ループ はタイトなループである。 つまり、スケジューラーがこのスレッド本体でスレッドを選択し実行させると、スレッドは決して自発的に CPU の制御を放棄しない。スレッドは、 while ループが自然に終了するまで、あるいはスレッドがより優先順位の高いスレッドで無効にされるまで、実行し続ける。

状況によっては、「利己的な」スレッドを持っても、より優先順位の高いスレッドが利己的なスレッドを無効にする(ちょうど RaceApplet での描画スレッドが利己的な走者を無効にするように)ので、問題は起きない。 しかし他の状況では、走者クラスのような CPU に貪欲な run()メソッドを持つスレッドが CPU を支配でき、他のスレッドは実行する機会を得るまで長い間待たなければならなくなる。

タイムスライス

いくつかのシステムでは、タイムスライスとして知られる戦略で、利己的なスレッドのふるまいに対抗する。 タイムスライスは、優先順位の等しい複数の「実行可能な」スレッドがあり、それらのスレッドが CPU を争う優先順位の最も高いスレッドである時に、活動し始める。 例えば、このスタンドアロン Java プログラム (上記の RaceApplet に基づく)は、次の run()メソッドを持つ優先順位の等しい2つの利己的なスレッド を作成する。

public void run() {
    while (tick < 400000) {
        tick++;
        if ((tick % 50000)
 == 0) {
            System.out.println("Thread #" + num + ", tick = " + tick);
        }
    }
}    

この run() は、整数 tick を増分するタイトなループを含む。そして50,000回の tick 毎に、スレッドの識別子とその tick 回数を出力する。

このプログラムをタイムスライスするシステム上で走らせると、両方のスレッドからのメッセージがお互いに混合しているのがわかる。 次のようになる。

Thread #1, tick = 50000
Thread #0, tick = 50000
Thread #0, tick = 100000
Thread #1, tick = 100000
Thread #1, tick = 150000
Thread #1, tick = 200000
Thread #0, tick = 150000
Thread #0, tick = 200000
Thread #1, tick = 250000
Thread #0, tick = 250000
Thread #0, tick = 300000
Thread #1, tick = 300000
Thread #1, tick = 350000
Thread #0, tick = 350000
Thread #0, tick = 400000
Thread #1, tick = 400000

これは、タイムスライスするシステムがタイムスロットに CPU を分割し、実行するタイムスロットを、等しくて最も高い優先順位を持つ各スレッドに反復して与えるためである。タイムスライスするシステムは、等しくて最も高い優先順位を持つスレッドのすべてに、少しの時間を繰り返し与える。それは、スレッドのいずれかが終了するか、あるいはより高い優先順位のものに無効にされるまで続く。スレッドが実行するように予定されている頻度や順序については、タイムスライスは保証しないので注意すること。

しかし、このプログラムをタイムスライスしないシステム上で走らせると、他のスレッドがメッセージを出力するチャンスを得るまでにはいつも、1つのスレッドからのメッセージ出力が終了しているのがわかる。 次のようになる。

Thread #0, tick = 50000
Thread #0, tick = 100000
Thread #0, tick = 150000
Thread #0, tick = 200000
Thread #0, tick = 250000
Thread #0, tick = 300000
Thread #0, tick = 350000
Thread #0, tick = 400000
Thread #1, tick = 50000
Thread #1, tick = 100000
Thread #1, tick = 150000
Thread #1, tick = 200000
Thread #1, tick = 250000
Thread #1, tick = 300000
Thread #1, tick = 350000
Thread #1, tick = 400000

これは、タイムスライスしないシステムが、実行するために優先順位が同等かつ最も高いスレッドを選択し、これによりそのスレッドは(そのジョブをスリープ、譲歩、終了することにより) CPU を放棄するまで、あるいはより高い優先順位がそれを無効にするまで、実行することができるためである。

試してみよう: あなたのマシンで RaceTest SelfishRunner クラスをコンパイルして実行しなさい。 タイムスライスするシステムを持っているかどうか分かるだろうか?

想像できるように、 CPU 集中型のコードを作成することは、同じ処理で実行する他のスレッドに否定的な影響を及ぼす。一般に、周期的に CPU を自発的に放棄して、他のスレッドに実行する機会を与える「行儀の良い」スレッドの作成を試みるべきである。 特に、タイムシェアリングに頼る Java コードは決して作成するべきではない。そのプログラムが異なるコンピュータシステムでは異なる結果を与えることは、保証されたも同然である。

スレッドは yield() メソッドを呼び出すことにより、自発的に(スリープしたりあるいは他の抜本的な手段を使わずに) CPU を譲ることができる。 yield() メソッドは同じ優先順位の他のスレッドに実行する機会を与える。 もし優先順位が等しく「実行可能な」状態のスレッドがなければ、譲歩は無視される。

試してみよう:  run() メソッドから yield() メソッドを呼び出して、SelfishRunner クラスを PoliteRunner に書き換えなさい。 SelfishRunners の代わりに PoliteRunners を作成するために、メインプログラム を必ず変更すること。 あなたのマシンで新規のクラスをコンパイルして、実行しなさい。 もっと良くなったか?

要約


Previous | Next | Trail Map | Writing Java Programs | スレッドの制御