Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

こちらのページは更新が滞っており、情報が古くなりつつあります。新しいCC情報サイトをはじめましたので、もしよければご参照ください。今後ともよろしくお願い申し上げます。

「百億のマインクラフトと千億のタートル」(https://hevo2.hatenablog.com/)

sleep実装からイベントを学ぶ(6)-Too long without yieldingエラーの解決方法

はじめに

CCプログラムを書いていると、必ず一度は出会う
「Too long without yielding」エラー

これを解決するために一般的に言われるのが、「sleep()を入れろ」

なぜ? sleep入れたらプログラムの実行が遅くなるじゃない。
ただでさえプログラム実行という重いModなのに、
プログラムをさらに遅くするなんてヤダヤダ。

なんて思っていたときもありました。

今回の記事は、このエラーについて解説したいと思います。
なお、本文は非常に長いです。エラーの理由に興味がなく、エラーを避けるためのTIPSだけを知りたいという方は、この記事の末尾の「まとめ」だけ読んでいただければ結構です。

「Too long without yielding」エラーをわざと起こす

以下のようなプログラムを実行すると、だいたい5秒前後で、「Too long without yielding」エラーが生じ、プログラムがとまります。

-- ただひたすらに画面にaを表示し続けるプログラム
while true do
  print("a")
end
-- 左から接続したレッドストーン信号がONになるまで何かをするプログラム
repeat 
  -- ここにいろいろ処理が書いてある
until rs.getInput("left")

sleep入れると解決する

このようなエラーが生じた場合は、以下のようにsleepを入れてあげることでたいてい解決します。

-- ただひたすらに画面にaを表示し続けるプログラム
while true do
  print("a")
  sleep(0) -- sleep 1tick (0.05sec)
end

でもこれまでの連載で、sleepは「タイマーイベント使って待つだけ」の関数であることがわかっています。
なぜこれで解決するのでしょう。

「Too long without yielding」の理由

(以下の理由についての文章は僕の想像と仮説が大部分です。特にCC作者の設計意図を推測した部分については大きく間違えている可能性があります。しかし意図はともかくとして実際になされた設計についてはおおよそ合っているはずです。このあたり識者の方のご指摘をお待ちしております)

Minecraft本体とそのModであるComputerCraft(CC)がJavaで書かれていることはみなさんご存知のとおりです。
そしてCCはユーザーが作ったLuaプログラムを実行できますが、
これはCC自体に、LuaJというJavaで書かれたLuaプログラム実行環境(Javaで書かれたLuaエンジン)が組み込まれているためです。

CCは、ひとつのLuaプログラムを実行するために、Javaスレッド生成→LuaJを使ってLuaプログラム実行という手順を繰り返していると思われます。
たくさんのタートルを同時に動かすことができるのはこれのおかげですね。たぶん。

しかしここで問題なのが、ユーザーが作ったLuaプログラムが無限ループ(あるいはそれにほぼ等しいくらい処理の重い)プログラムである場合です。
Javaスレッドを使ってLuaプログラムを実行したはいいけれど、いつまでもプログラムが終わらず、特定のスレッドが残り続けている。
Java側から見るとスレッドがいつまでも残っているのは困りますよね。計算機資源の無駄です。
「まだLuaプログラム終わらないの? もしかしてこれ無限ループのプログラムなの? それとも普通にプログラム実行中なの? まさかバグでプログラムが暴走していたりしないよね? ねぇこのスレッド生きているの?死んでいるの?答えてよ!」

この事態の解決方法として、
Luaプログラムを呼び出して実行したらそのまま処理が終わるまで延々と待ち続けるという放任主義戦略ではなく、一定時間だけLuaプログラムを実行したら途中で中断し問題ないことを確認したらまたその続きを実行するという動作を繰り返す戦略、つまりは小出し戦略を採ることにしました。
頻繁に制御がLuaプログラム呼び出し側(つまりはJava側)に戻るのですから、不測の事態にも対応できますよね。

しかしこれにも問題があります。途中で中断するというけれど、どのタイミングで中断すればいいのでしょう。Java側の勝手なタイミングで中断されると困りますよね。
たとえば、線画をアニメーションさせるLuaプログラムで勝手なタイミングで処理が中断されると、たとえすぐ処理が再開するにせよ、タイミングが狂うわけですから動きがカクカクになってしまいます。

そこで中断のタイミングは、Java側が勝手に判断するのではなく、CCプログラマ(CCユーザー)側に任せることにしました。
プログラム中で中断しても良いタイミングを、CCプログラマ側が指定するわけです。

では、その指定はどうやって行うのでしょう。
そしてそもそも、「Lua側が指定したタイミングで処理を中断」「中断した場所からまたその続きを再開する」という機構はどうやって実現しているのでしょうか。

Luaの独自機構、コルーチン

そのための機構として、Luaにはcoroutine(コルーチン)という便利な機能(関数)が実装されています。さすがLuaさん。組み込み用として設計されているのは伊達ではありません。
詳しく説明すると長くなるので誤解を恐れず簡潔にまとめますが、これはようするに、「外部プログラムがLuaプログラムを呼び出して実行したとする。もしそのLuaプログラム内にコルーチン関数が含まれていたら、その場所で一時的にLuaプログラムの処理を中断し、外部プログラムに処理を戻す」という機能です。
処理を中断とか処理を抜けるというとbreakやreturnを思いつきますが、コルーチンには「中断時の状態が完全に保持されるため中断したその状態その場所から何事もなかったかのように再スタートできる」という、非常に強力で便利な特徴があります。

CCの作者であるdan200氏は、「Luaプログラムは、定期的にコルーチン関数を使ってJava側に制御を戻さなくてはならない」というルールを作りました。
そうしなければ、「Too long without yielding」エラーで処理をとめるぞと。
ユーザーである私たちはその意向に従わなくてはなりません。

「ええええ。定期的に中断することを意識しながらプログラミングするの? めんどくさい」 そう思ったあなた。ご安心ください。
dan200氏は、ただの中断の目印としてコルーチン機構を採用するのではなく、重要な役割をもたせた上で採用しました。
この重要な役割を果たす、よく使われる関数の中にコルーチン関数が埋め込まれているので、プログラム中に(比較的)自然にコルーチンが入り込むようになっています*1。

具体的にどの関数にコルーチン関数が埋め込まれているのか、示しましょう。
以前、sleep()が定義されているファイルとして、「ComputerCraft1.53/lua/bios.lua」を紹介しました。

この中に、次のような記述があります。

function os.pullEventRaw( _sFilter )
	return coroutine.yield( _sFilter )
end

function os.pullEvent( _sFilter )
	local eventData = { os.pullEventRaw( _sFilter ) }
	if eventData[1] == "terminate" then
		error( "Terminated", 0 )
	end
	return unpack( eventData )
end
  • os.pullEventRaw()は、コルーチンの別名関数
  • これまで何度も使ってきたos.pullEvent()は、os.pullEventRaw()に"terminate"イベント(CTRL+T)に関する処理を付け加えたもの*2

「つまり、os.pullEvent()は、コルーチンそのものだったんだよ!」
「えええ、なんだってーー(AA略)」

os.pullEvent()と言えばイベントですよね。
つまりCCのイベント関連の機能は、コルーチンによって実装されているのです。

os.pullEvent()のはたらき

CC本体のソースを見ることができないのであくまで予想ですが、
外部プログラム、おそらくJava側が各種イベントの発生を管理しているはずです。
コルーチンでLuaの処理を中断しJava側に戻ってきたらイベントが発生するまでそのまま待機。
そして何らかのイベントの発生とともに、再度その中断した位置からLuaプログラム処理を再開しているのだと思われます。

これまで、「os.pullEvent()はイベントを拾う関数」と説明してきましたが、
もっと正確に言うならば、「os.pullEvent()はLuaの処理を中断してJava側に制御を戻す。そしてイベント発生とともに処理を再開する」関数と説明するのが正しいのでしょう。たぶん。おそらく。

そのため、イベントドリブンな(なんらかのイベントをトリガーとして動きだす)プログラムであれば、os.pullEvent()が、ついてはコルーチンがプログラム中に自然と含まれるようになります。
もし、イベントを必要としないプログラムを作るときは、意識してos.pullEvent()を使うようこころがけましょう。
sleep()はタイマーイベントを使っているので、sleepを使えば自動的にos.pullEvent()を使うことになります。手軽なのでお勧めです。

今回のまとめ

またも長々と書きました。初心者の方には難しい部分もあると思います。
その場合は、以下だけでも覚えてください。

  • CCでプログラム組むときには定期的にコルーチン(coroutine.yeild)を使え!
  • さもないと、「Too long without yielding」と怒られるぞ
  • コルーチンが何なのかわからなくても大丈夫。os.pullEvent()のことだから。
  • 別に俺のプログラムでイベント処理する気はないんだけど ←だったらsleep使いな!
  • ええええ、sleep使ったらプログラムの動作が遅くなるじゃん ← sleep(0)使え! 0.05秒くらい我慢しなさい。
  • 定期的にと言うけれど、実際にどのくらいの時間ごとなのか・・・・・・。体感では5秒くらい?
  • 実は、他にも、このエラーを避ける方法があります。それについてはまた次回。

*1:Lua組み込みを使っているゲーム(処理系)は他にも数多くあるようですが、それらの例を知らないのでこの設計が一般的なのかどうかはわかりません。しかし、この設計思想が優れていることは確かです。

*2:errorという関数が使われていますが便利そうですね。今度使ってみましょう。