Lua
LuaDay 6

Luaでコルーチン

More than 3 years have passed since last update.


Luaでコルーチン

遅刻してしまいましたが、Lua Advent Calendar 6日目書きます。

その拡張性と実行速度からしばしばゲーム開発において用いられます。

その中でも特に強力な機能としてコルーチンがあります。


コルーチンとは

強調的マルチスレッドとも呼ばれます。

マルチスレッドはここであえて説明する必要はないとは思いますが、処理をスレッドという単位にわけ平行で実行し、パフォーマンスを向上させるというものです。

マルチスレッドでは実行するスレッドの切り替えを自動で行うのに対し、コルーチンは手動で切り替えを行います。

明示的にスレッドの切り替えを行わない限り他のスレッドの処理が割り込むことはありません。


どういうときに使うのか

パフォーマンス向上目的というよりは、非同期処理をシンプルに書くために使われることが多いかと思います。

たとえばこういうケースです。


  1. 保存ダイアログを表示する

  2. 保存ダイアログのOKボタンが押されれば保存して終了する

  3. 保存ダイアログのCANCELボタンが押されれば確認ダイアログを表示する

  4. 確認ダイアログのOKボタンを押せばアプリケーションをを終了する

  5. 確認ダイアログのCALCELボタンが押されれば再度保存ダイアログを表示する

コールバックを活用し、いかのようなコードをおそらく書くことになります。

local showSaveDialog

local showConfirmDialog

showSaveDialog = function()
_showSaveDialog({
onOK = function()
save()
exit()
end,
onCANCEL = function()
showConfirmDialog()
end
})
end

showConfirmDialog = function()
_showConfirmDialog({
onOK = function()
exit()
end,
onCANCEL = function()
showConfirmDialog()
end
})
end

ですがもう一度よく処理の流れを見てください



  1. 保存ダイアログを表示しする

  2. 保存ダイアログのOKボタンが押されれば保存して終了する

  3. 保存ダイアログのCANCELボタンが押されれば確認ダイアログを表示する

  4. 確認ダイアログのOKボタンがあればアプリケーションをを終了する

  5. 確認ダイアログのCANCELボタンが押されれば再度保存ダイアログを表示する


この処理、普通に無限ループで書くことができます。

ですが何故コールバックで書かないといけないのでしょうか?

それはこの処理がノンブロッキングだからです。

showSaveDialog()のあとすぐにshowConfirmDialog()を書いてしまうと2つのダイアログが同時に出現してしまいます。

なのでコールバックで実装するしかないのです。

しかし、これだと処理の流れが複雑になってしまいます。

今回の場合はダイアログが2つだけでしたが3つ、4つと増えてくるとコールバックが増え、非常に見通しが悪くなってしまいます。

それを解決してくれるのがコルーチンです。

コルーチンを使うと以下のように書き換えることができます。

local showSaveDialog

local showConfirmDialog

local thread = coroutine.create(function()
while true do
local ok_pushed
ok_pushed = showSaveDialog()
if ok_pushed then
save()
return
end

ok_pushed = showConfirmDialog()
if ok_pushed then
return
end
end
end)

showSaveDialog = function()
_showSaveDialog({
onOK = function()
coroutine.resume(thread, true)
end,
onCANCEL = function()
coroutine.resume(thread, false)
end
})
coroutine.yield(thread)
end

showConfirmDialog = function()
_showConfirmDialog({
onOK = function()
coroutine.resume(thread, true)
end,
onCANCEL = function()
coroutine.resume(thread, false)
end
})
coroutine.yield(thread)
end

coroutine.resume(thread)

このように同期的な無限ループに書き換えることができます。

ダイアログ表示のタイミングでスレッドを一時停止しているのでビジーループにはなっていません。

やっていることは簡単でcoroutine.createで新しいスレッドを作成。

coroutine.yieldでスレッドの処理を中断し、元のスレッドに戻る。

coroutine.resumeでスレッドの処理を再開する。

ダイアログの表示は別スレッドで行い、表示した瞬間スレッドを停止、ボタンが押されたらスレッドを再開させる、それだけです。

コールバックよりは見通しがよくなったと思いませんか?


まとめ

このようにコルーチンを使うとこのように非同期処理をすっきり書くことができます。

一方でLuaのホストとしてはしばしばC/C++が用いられますが、そこまで非同期処理につよいとはいえません。

この辺うまくすみわけできるとよいですね。