チュートリアル 08 スレッド(Threads)

ユーザの視点から見ると、モダンなオペレーティングシステムでは、同時に多くのプログラムをいっせいに実行することが可能です。しかし、本当のところは、オペレーティングシステムに登録された個々のCPUは1度に処理の1スレッドを扱うことしかできません。まるで、同時に実行されているかのように見えるのは、オペレーティングシステムが数多くのアクティブなスレッドを、すばやく、「ジャグリング」のように交互に実行しているためです。スレッドの実行順序を決定するスケジューリングアルゴリズムの分析と設計は、コンピュータ科学の研究における実り多い領域です。

個々のプログラムの中には、数多くの実行スレッドが含まれる場合があります。例えば、Web ブラウザはネットワーキングを扱うスレッドを1つ持ち、それとは別にユーザ入力を取り扱うスレッドを持っているでしょう。プログラムのこのような部分を別のスレッドに置いておくことにより、プログラムがユーザ入力を処理(例えば、選択を決定)し、他のスレッドはその要求が完了するまで処理(web ページのコンテンツの取得)を待たせることができます。

mxj を使用している場合、Max 環境からの入力や、Max 環境への出力は、1 〜 2 スレッドで行なわれます。メインスレッドは、最もスピードが重要視されない処理を扱います。これには、スクリーンのレンダリング、キーボードやマウスのイベントの処理などがあります。スケジューラスレッドは時間的に敏感なデータを扱います。これには、MIDI データ、クロックに関係したオブジェクト(metro、delay など)があります。

次のコードは、classes ディレクトリの中にある WhichThread からの例です。

import com.cycling74.max.*;

public class WhichThread extends MaxObject implements Runnable
{
    private static final String SCHEDULER = "SCHEDULER";
    private static final String MAIN      = "MAIN";
    private static final String UNKNOWN   = "????";

   	public void bang() {
		if(MaxSystem.inTimerThread())
		    outlet(0,SCHEDULER);
		else if(MaxSystem.inMainThread())
		    outlet(0,MAIN);
		else
		    outlet(0,UNKNOWN);
	}

    public void run() {
		this.bang();
    }

    public void spawnThread() {
		Thread t = new Thread(this);
		t.start();
    }
}

bang メソッドでは、 MaxSysteminTimerThread メソッドとinMainThread メソッドを使って実行スレッドを判定する方法を示しています。実行されたスレッドがどちらになるかによって、異なった出力処理が行なわれます。WhichThread.help をいろいろ試してみるとよく理解できるでしょう。

ヘルプパッチの任意のbang をクリックすると、メインスレッドで bang メソッドが呼び出され、"MAIN"が出力されます。オーバードライブが有効になっている場合、パッチにある metro をスタートさせると、スケジューラスレッドでbangメソッドが呼び出され、"SCHEDULER" が出力されます。オーバードライブをオフに切り替えると "MAIN"が出力される点に注意して下さい。

また、WhichThread は クラスが自分自身のスレッドを作成する1つの方法を示しています。このクラスが Runnableを実装している点に注意してください。このクラスは、前のチュートリアルで説明したExecutableインターフェイスと非常に良く似たもので、スレッドベースのクラスのために、共通な呼び出しのフレームワークを提供します。Runnable をインプリメント(実装)するためには、クラスの中に、スレッドがスタートしたときに実行されるパブリックな run メソッドを置かなければなりません。上の例では、spawnThread が呼び出されると、 Thread クラスの新しいインスタンスが作られ、"this" キーワードによってWhichThread オブジェクト自身がコンストラクタメソッドに渡されています。その次の行では、この新しい Threadstart メソッドが呼び出され、これによって、Thread に対して実行を開始するよう命じています。start メソッドは Runnablerun メソッドを呼び出します。 Runnable は生成された時点で渡されていたもので、 オブジェクト自体の中でこれを渡しています。そのため、WhichThreadrun メソッドが呼び出され、これによって bang メソッドが呼び出されます。上の例では、このようにして、メインスレッドでもスケジューラスレッドでもないスレッドから呼び出されているため 、 "????" が出力されます。

ヘルプパッチの "spawnThread" メッセージをクリックすると、実際にオブジェクトのアウトレットから "????" が出力されることに気がつくでしょう。しかし、同時にパッチの右側では、第1のWhichThread オブジェクトからの出力を受け、これによって bang がトリガされ、もう1つのWhichThread オブジェクトに送られていることにも気がつくと思います。"spawnThread" メッセージをクリックしたとき、この動作を開始させたスレッドはメインスレッドでも、スケジューラスレッドでもないため、この第2の WhichThread の出力もまた "????" になると推測するでしょう。しかし、そうではなく "MAIN" が出力されます。このことは第2のWhichThread で受信した2番目のbang がメインスレッドで実行されたことを表しています。これは、MaxObject's の outlet メソッドがメインスレッド、あるいはスケジューラスレッドのどちらかでのみMax 環境に対してメッセージを送信するためです。これら2つのスレッド内で呼び出されたのではない outlet の呼び出しの場合でも、この呼び出しはメインスレッドまで据え置かれます。

スレッドが他のスレッドによっていつでも割り込まれる可能性があるということを理解しておくことは重要です。例えば、オペレーティングシステムが、メインスレッドではなくスケジューラスレッドに処理を渡すべき時間であると判断すれば、メインスレッドの実行は延期され、スケジューラスレッドがその処理を横取りします。メインスレッドが再開される場合、処理を停止した場所から正確に実行されます。これは、若干の不安定な状況を招くことがあります。次のようなクラスを考えてみて下さい。


public class threadProblem extends MaxObject {
	private int[] data = new int[10];
	
	public void bang() {
		int size = data.length;
		for (int i=0;i size;i++) {
			data[i] = i;
		}
	}
		
	public void inlet(int i) {
		data = new int[i];
	}
}

オブジェクトのインレットに送られた整数は、与えられた長さで新しい配列を作り、bang によって配列の数値が生成されます。しかし、メインスレッドで実行された(例えば、ユーザによって入力された)、bangが、bang メソッドの実行中に割り込まれ、スケジューラスレッドで整数が入力された場合どのようなことが起こるでしょうか?入力された整数値が前回入力時の配列のサイズより小さいとき、for ループが新しい配列の長さよりも大きな値に達していた場合には、処理の流れがメインスレッドに戻された直後に例外が発生します。入力された整数値が前回の配列のサイズより大きいときには例外は発生しませんが、それまでの長さを超えた配列の値、あるいは、すでにfor ループによってセットされていた値は初期化されません。どちらの場合でも、結果は期待しないものになってしまいます。このことから、予期しない処理の流れによってデータが不定の状態に陥らないようにクラスを設計することは重要なことであるといえます。