Euphoriaのマルチタスク


はじめに

Euphoriaは複数の独立したタスクを構成することができます。タスクごとに固有の実行対象の命令文と、固有のサブルーチンのコールスタック、固有のプライベート変数を一式保有しています。タスクは他のタスクと並列で実行されます。すなわち、任意のタスクの作業を完了する前に、他のタスクを実行する機会を与えることができます。Euphoriaのタスクスケジューラは、どのタスクが指定時間に動作するべきか決定します。


なぜマルチタスク?

多くのプログラムにおいてはマルチタスクを使用する必要はないため有益ではありません。しかし、場合によっては非常に有用です:

  • アクションゲームでは多数のキャラクターや砲弾などは、現実な方法で、全て独立した別々の個体として表示される必要があります。たとえば言語戦争ゲームが好例です。

  • 時々、プログラムは利用者や他のコンピュータから入力待ちする必要がある状況になることがあります。プログラムのタスクが入力待ちをしているとき、別の独立したタスクは複数の計算、ディスク検索などを行うことができます。

  • Windows, Linux/FreeBSDは全てI/Oから開始できる特別なAPIルーチンがいくつかあり、それらが終了する待つことなく処理できます。タスクは別のタスクが有用な計算を実行中、また別のI/O操作が開始されたときに、I/O処理が完了したか定期的に監視することができます。

  • 状況として多数の利用者からプログラムが同時に呼ばれることがあります。複数のタスクで、全て別々の利用者相互の状態を追い続けるのは容易です。

  • 恐らくプログラムを2つの論理処理に分けて、タスクごとに実行できるでしょう。他のタスクがデータを読み込んで処理している間に、データを作成および保存することができます。最初の処理は最優先で実行されるでしょうが、利用者と対話するといった次の処理が実行でき、次の処理が実行中に一時休止している間は利用者は何か考えていたり別の作業をしているなどの理由で即答を要求されません。


タスク方式

Euphoriaは2種類のタスク方式に対応しています: リアルタイムタスク(同期あるいは実時間タスク)、タイムシェアタスク(時間共有あるいは非同期タスク)

リアルタイムタスクは指定された秒数または小数の間隔でスケジュールが行われます。単一のリアルタイムタスクを3秒ごとに実行する間、別のタスクが0.1秒ごとに実行されるようにスケジュールすることがあります。言語戦争ゲームでは、Euphoria船が4つ先へ移動するためにワープしたり、魚雷が飛行して画面を横断するとき、これらを一定した速度で安定動作させることは重要です。

タイムシェアタスクはCPUを共有する必要がありますがクロックに基づいた厳密なスケジュールは一切不要です。4タスク併用によるソートデモ demo\dos32\tasksort.ex ではCPUを共有しますが、それは特定の時間にスケジュールされることは重要ではありません。

任意の時間にタスクを再スケジュールすることは可能であり、それによりCPUのタイミングまたはスライスを変更できます。動的にタスク種別を別のものに変換することができます。


小例

この例はメインタスク(全てのEuphoriaプログラムの開始地点)に2つのリアルタイムタスクを追加するために作成します。数秒ごとに制御権を得られようスケジュールするためにリアルタイムタスクにて呼び出します。

この例をコピー/ペーストして試しに実行してください。task1は2.5から3秒ごとに制御権を得る間に、task2は5から5.1秒ごとに制御権を得ていることが観察できます。その間に、メインタスク(タスク0)は実行を終了するための制御として文字'q'が入力されるまでキーボード入力を監視を続けます。

constant TRUE = 1, FALSE = 0

type boolean(integer x)
	return x = 0 or x = 1
end type

boolean t1_running, t2_running

procedure task1(sequence message)
	for i = 1 to 10 do
		printf(1, "task1 (%d) %s\n", {i, message})
		task_yield()
	end for
	t1_running = FALSE
end procedure

procedure task2(sequence message)
	for i = 1 to 10 do
		printf(1, "task2 (%d) %s\n", {i, message})
		task_yield()
	end for
	t2_running = FALSE
end procedure

puts(1, "main task: start\n")

atom t1, t2

t1 = task_create(routine_id("task1"), {"Hello"})
t2 = task_create(routine_id("task2"), {"Goodbye"})

task_schedule(t1, {2.5, 3})
task_schedule(t2, {5, 5.1})

t1_running = TRUE
t2_running = TRUE

while t1_running or t2_running do
	if get_key() = 'q' then
		exit
	end if  
	task_yield()
end while

puts(1, "main task: stop\n")
-- メインタスク完了後にプログラムは終了します。


初期のマルチタスク機構との比較

Euphroiaの初期のリリースの言語戦争ゲームでは、既にマルチタスク機構が実装されており、さらに一部の人々は自身で開発したマルチタスク機構をUser Contributionsページに投稿していました。これら全ては平易なEuphoriaコードを使用して実装されておりましたが、新規にマルチタスク機構のインタプリタに内蔵されました。古い言語戦争ゲームのタスク機構はスケジューラがタスクを*呼び出して*、いずれスケジューラに制御を*返す*といったように、次のタスクへ切り替えていきます。

新刷されたシステムでは、タスクは組み込み手続きtask_yield()を任意の地点で呼び出すことができ、恐らく多重サブルーチン呼び出し、およびスケジューラは現在はインタプリタの一部であり、制御権を任意の他のタスクに移行するこができます。元のタスクに制御権を復帰するとき、コールスタックと全てのプライベート変数に影響なくtask_yield()命令文の後に元のタスクの実行を再開します。各タスクは自身のコールスタック、プログラムカウンタ(すなわち現在実行中の命令文)、およびプライベート変数を所有しています。複数のタスクがルーチンが全て同時に実行している場合があり、さらに各タスクは自身のルーチンのためにプライベート変数の値を一式所有しています。グローバルおよびローカル変数はタスク間で共有されます。

任意のコードの一部分をタスクとして実行することはきわめて簡単です。なお、CPUの独占を阻止するためにtask_yield()命令文を数箇所挿入します。


マルチスレッドとの比較

人々がスレッドについて論ずるとき、通常オペレーティングシステムにより提供されている仕組みについて触れます。誤解を避けるという理由で"マルチタスク"という用語の使用を選びました。通常においてスレッドは"優先的(Preemptive)"ですが、Euphoriaのマルチタスクは"協調的(Cooperative)"です。優先的スレッドでは、実際のところオペレーティングシステムが任意の時間に別のスレッドへの切り替えを強制的することができます。協調的マルチタスクでは、各タスクは別のタスクに制御権とCPU時間をいつ与えるか決定できます。タスクが"強欲"ならば自身が長時間CPU時間を独占できます。 しかし個人または団体によって記述されたプログラムがプログラムとして上手く機能して欲しいとき、単一のタスクが独占をすることは好ましくはありません。これらは利用者のために上手く動作する方法で均衡を取ろうとします。オペレーティングシステムは多くのスレッドと様々な人々によって開発された多数のプログラムを実行している場合があり、これらのプログラムで正当な範囲の資源共有を強制することは有益です。優先権は全てのオペレーティングシステムの全体で意味があります。しかし、単一のプログラム内ではあまり意味はありません。

さらに、スレッドは悪名高い陰険なバグを発生させるがことあります。タスクが誤って制御権を失ったときに凄惨なことが発生します。制御権を失ったときグローバル変数の更新が発生して矛盾した状態の変数が残るかもしれません。何かの理由で誤ってスレッド切り替えが発生した瞬間に変数の加算を失敗することは容易に起きます。すなわち、2つのスレッドを例に考えてみましょう。まず一つ目はこうです:

     x = x + 1

その他はこうです:

     x = x + 1

マシンレベルでは、最初のタスクはxの値をレジスターにロードしますが、xに加算を行うはずの次のタスクは制御権を失ってしまい、メモリー内に格納したxを結果として返してしまいます。最終的には最初のタスクがxを加算するために*レジスター内のxの値を使用して*、メモリーにxの結果が格納されます。これは故意にxは2回ではなく1回だけ加算されます。この問題を避けるには、スレッドごとに次のようなものが必要です:

     lock x
    x = x + 1
    unlock x

lockおよびunlockは特別かつ原始的なスレッドセーフ機構です。しばしばプログラマがデータのロックするのを忘れる場合がありますが、この場合プログラムは正常に動作しているように見えます。しかし、コードを書いた後日のとある日または数ヵ月後にプログラムは不可解なことに暴走します。

協調型マルチタスクは非常に安全で、高価なロック操作において必要とされるコストが少ないです。論理操作を完了するとタスクは安全になった時点で制御権を放棄します。


内蔵マルチタスクルーチン

これら全てのルーチンはEuphoriaに内蔵されており、ライブラリファイルのインクルードは一切不要です。

task_create - これは新しいタスクを作成するため呼び出します。最初に手続きに対して引数のリストを渡すのと同様に、Euphoriaの手続きをルーチンIDとして渡す必要があります。これはタスクにおいてメインの手続きです。タスクで値を返すことは意味が無いため(他のタスクを待つことなく)、タスクは常に手続きです。task_create()はタスクID(小さな整数)を返します。このタスクIDは、後述するほかのマルチタスクルーチンでタスクを識別するために使用します。注意事項として全てのEuphoriaプログラムは最初に1つのタスクから実行されるということです。
task_yield - これはタスクに制御権を移譲するため呼び出すものであり、これによりEuphoriaスケジューラは実行対象のタスクを新しく選択できます。タスクがCPUを独占するのを避けるために時々呼び出す必要はありますが、大量の時間がスケジューラーに消費されてしまうほど頻繁に呼び出さないでください。データ構造の破壊を避けるために、タスクは制御権を放棄する前に論理処理を完了する必要があります。同一タスクの実行を継続したまま他のタスクが制御権を得たり起動されたりすることなくtask_yield()からスケジューラに迅速に復帰する可能性があります。
task_schedule - タスクを実行するスケジュールを決定します。タスク作成後に、作成したタスクをスケジュールするために必要であり、指定しなければ実行されません。タスクをスケジュールするためには主に2つ方法があります。一つ目の方法はリアルタイムで実行間隔の最小...最大値を指定します。これはタスクを現在から最小秒数まで、および現在から最大秒数まで(可能であれば)スケジュールする必要があるということをスケジューラに指定します。以降のタスクの実行は最小/最大秒数の間は待機します。タスクが3.5から4.0秒ごとに実行するように指定することがあります。次はタスクがtask_yield()を実行して指定秒数になる前にCPUを開放することができるタイムシェアリングシステムを使用する方法です。これは一つのタスクが10回のtask_yields()を実行する場合があり、可能であればどこか別のタスクとしておそらく優先度の低いタスクはtask_yield()が実行されるごとにCPUを開放する必要があります。
task_list - 全タスクのリストを取得します。
task_self - 現在タスクのタスクIDを返します。
task_status - 現在のタスクの状態(動作、休止、停止)を取得します。
task_suspend - 通知があるまでタスクを休止します。
task_clock_start - スケジューラのクロックを再起動します。リアルタイムクロックが中断中に状態を保持する必要があるゲームアプリケーションで使用されます。
task_clock_stop - スケジューラのクロックを停止します。