Euphoriaのマルチタスク
Euphoriaは複数の独立したタスクを構成することができます。タスクごとに固有の実行対象の命令文と、固有のサブルーチンのコールスタック、固有のプライベート変数を一式保有しています。タスクは他のタスクと並列で実行されます。すなわち、任意のタスクの作業を完了する前に、他のタスクを実行する機会を与えることができます。Euphoriaのタスクスケジューラは、どのタスクが指定時間に動作するべきか決定します。
多くのプログラムにおいてはマルチタスクを使用する必要はないため有益ではありません。しかし、場合によっては非常に有用です:
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 lockおよびunlockは特別かつ原始的なスレッドセーフ機構です。しばしばプログラマがデータのロックするのを忘れる場合がありますが、この場合プログラムは正常に動作しているように見えます。しかし、コードを書いた後日のとある日または数ヵ月後にプログラムは不可解なことに暴走します。 協調型マルチタスクは非常に安全で、高価なロック操作において必要とされるコストが少ないです。論理操作を完了するとタスクは安全になった時点で制御権を放棄します。
これら全てのルーチンはEuphoriaに内蔵されており、ライブラリファイルのインクルードは一切不要です。
|