在 Codea
中, 函數 draw()
缺省每秒執行 60
次, 咱們但願能修改一下它的刷新速度, 因而想到了 Lua
的一個特性:協程 coroutine
, 但願試着用它來控制程序執行的節奏, 不過目前對於協程還不太瞭解, 那就一邊看教程, 一邊試驗好了.多線程
咱們知道, Codea
的運行機制是這樣的:框架
setup()
只在程序啓動時執行一次draw()
在程序執行完 setup()
後反覆循環執行, 每秒執行 60
次touched()
跟 draw()
相似, 也是反覆循環執行簡單說, 就是相似於這樣的一個程序結構:函數
setup() while true do ... draw() touched(touch) ... end
Lua
所支持的協程
全稱被稱做協同式多線程
(collaborative multithreading
)。Lua
爲每一個 coroutine
提供一個獨立的運行線路。然而和多線程不一樣的地方就是,coroutine
只有在顯式調用 yield
函數後才被掛起,再調用 resume
函數後恢復運行, 同一時間內只有一個協程正在運行。.net
Lua
將它的協程函數都放進了 coroutine
這個表裏,其中主要的函數以下:線程
使用 coroutine.create(f)
能夠爲指定函數 f
新建一個協程 co
, 代碼以下:code
-- 先定義一個函數 f function f () print(os.time()) end -- 爲這個函數新建一個協程 co = coroutine.create(f)
一般協程的例子都是直接在 coroutine.create()
中使用一個匿名函數做爲參數, 咱們這裏爲了更容易理解, 專門定義了一個函數 f
.orm
爲何要經過協程來調用函數呢? 由於若是咱們直接調用函數, 那麼從函數開始運行的那一刻起, 咱們就只能被動地等待函數裏的語句徹底執行完後返回, 不然是沒辦法讓函數在運行中暫停/恢復
, 而若是是經過協程來調用的函數, 那麼咱們不只可讓函數暫停在它內部的任意一條語句處, 還可讓函數隨時從這個位置恢復運行.協程
也就是說, 經過爲一個函數新建協程, 咱們對函數的控制粒度從函數級別精細到了語句級別.blog
咱們能夠用 coroutine.status(co)
來查看當前協程 co
的狀態教程
> coroutine.status(co) suspended >
看來新建的協程默認是被設置爲 掛起-suspended
狀態的, 須要手動恢復.
執行 coroutine.resume(co)
, 代碼以下:
> coroutine.resume(co) 1465905122 true >
咱們再查看一下協程的狀態:
> coroutine.status(co) dead >
顯示已經死掉了, 也就是說函數 f
已經執行完了.
有人就問了, 這個例子一會兒就執行完了, 協程只是在最初被掛起了一次, 咱們如何去手動控制它的掛起/恢復
呢? 其實這個例子有些太簡單, 沒有很好地模擬出適合協程發揮做用的使用場景來, 設想一下, 咱們有一個函數執行起來要花不少時間, 若是不使用協程的話, 咱們就只能傻傻地等待它執行完.
用了協程, 咱們就能夠在這個函數執行一段時間後, 執行一次 coroutine.yield()
讓它暫停, 那麼如今問題來了, 運行控制權如何轉移? 這個函數執行了一半了, 控制權還在這個函數那裏, 辦法很簡單, 就是把 coroutine.yield()
語句放在這個函數裏邊(固然, 咱們也能夠把它放在函數外面, 不過那是另一個使用場景).
咱們先把函數 f
改寫成一個須要執行很長時間的函數, 而後把 coroutine.yield()
放在循環體中, 也就是讓 f
每執行一次循環就自動掛起:
function f () local k = 0 for i=1,10000000 do k = k + i print(i) coroutine.yield() end end
看看執行結果:
> co = coroutine.create(f) > coroutine.status(co) suspended > coroutine.resume(co) 2 true > coroutine.status(co) suspended > coroutine.resume(co) 3 true > coroutine.status(co) suspended > coroutine.resume(co) 4 true >
很好, 完美地實現了咱們的意圖, 可是實際使用中咱們確定不會讓程序這麼頻繁地 暫停/恢復
, 通常會設置一個運行時間判斷, 好比說執行 1
秒鐘後暫停一次協程, 下面是改寫後的代碼:
time = os.time() timeTick = 1 function f () local k = 0 for i=1,10000000 do k = k + i print(i) -- 若是運行時間超過 1 秒, 則暫停 if (os.time() - time >= timeTick) then time = os.time() coroutine.yield() end end end co = coroutine.create(f) coroutine.status(co) coroutine.resume(co)
代碼寫好了, 可是運行起來表現有些不太對勁, 剛運行起來還正常, 但以後開始手動輸入 coroutine.resume(co)
恢復時感受仍是跟以前的同樣, 每一個循環暫停一下, 認真分析才發現是由於咱們手動輸入的時間確定要大於 1
秒, 因此每次都會暫停.
看來咱們還須要修改一下代碼, 那就再增長一個函數來負責自動按下恢復鍵, 而後把段代碼放到一個無限循環中, 代碼以下:
time = os.time() timeTick = 1 function f () local k = 0 for i=1,10000000 do k = k + i -- print(i) -- 若是運行時間超過 timeTick 秒, 則暫停 if (os.time() - time >= timeTick) then local str = string.format("Calc is %f%%", 100*i/10000000) print(str) time = os.time() coroutine.yield() end end end co = coroutine.create(f) function autoResume() while true do coroutine.status(co) coroutine.resume(co) end end autoResume()
鑑於 os.time()
函數最小單位只能是 1
秒, 雖然使用 1
秒做爲時間片有助於咱們清楚地看到暫停/恢復
的過程, 可是若是咱們想設置更小單位的時間片它就無能爲力了, 因此後續改成使用 os.clock()
來計時, 它能夠精確到毫秒級, 固然也能夠設置爲 1
秒, 把咱們的時間片設置爲 0.1
, 代碼以下:
time = os.clock() timeTick = 0.1 print("timeTick is: ".. timeTick) function f () local k = 0 for i=1,10000000 do k = k + i -- print(i) -- 若是運行時間超過 timeTick 秒, 則暫停 if (os.clock() - time >= timeTick) then local str = string.format("Calc is %f%%", 100*i/10000000) print(str) time = os.clock() coroutine.yield() end end end co = coroutine.create(f) function autoResume() while true do coroutine.status(co) coroutine.resume(co) end end autoResume()
執行記錄以下:
Lua 5.3.2 Copyright (C) 1994-2015 Lua.org, PUC-Rio timeTick is: 0.1 Calc is 0.556250% Calc is 1.113390% Calc is 1.671610% Calc is 2.229500% Calc is 2.787610% Calc is 3.344670% Calc is 3.902120% Calc is 4.459460% Calc is 5.017040% ...
好了, 關於協程, 咱們已經基本瞭解了, 接下來就要想辦法把它放到 Codea
裏去了.
上面那個例程中, 設置的時間片越小, 程序的控制權切換得越頻繁, 這一點剛好能夠用來設置 Codea
的屏幕刷新速度.
首先把那些只運行一次的函數和語句放到 setup()
中, 其次把那些須要反覆執行的函數和語句放到 draw()
中, 這裏須要稍做修改, 由於 Codea
的 draw()
自然地就是一個大循環, 因此咱們能夠考慮把 autoResume()
函數中的循環去掉, 把它的循環體放到 draw()
中就好了, 代碼以下:
function setup() time = os.clock() timeTick = 1/2 print("timeTick is: ".. timeTick) co = coroutine.create(f) end function draw() background(0) autoResume() sysInfo() end function f () local k = 0 for i=1,10000000 do k = k + i -- print(i) -- 若是運行時間超過 timeTick 秒, 則暫停 if (os.clock() - time >= timeTick) then local str = string.format("Calc is %f%%", 100*i/10000000) --print(str) time = os.clock() coroutine.yield() end end end function autoResume() coroutine.status(co) coroutine.resume(co) end -- 顯示FPS和內存使用狀況 function sysInfo() pushStyle() -- fill(0,0,0,105) -- rect(650,740,220,30) fill(255, 255, 255, 255) -- 根據 DeltaTime 計算 fps, 根據 collectgarbage("count") 計算內存佔用 local fps = math.floor(1/DeltaTime) local mem = math.floor(collectgarbage("count")) text("FPS: "..fps.." Mem:"..mem.." KB",650,740) popStyle() end
這樣咱們就能夠經過修改時間片 timeTick
的值來控制 draw()
函數的刷新速度了, 默認狀況下 draw()
是 1/60
秒刷新一次, 因此咱們可使用 1/60
來試驗, 這時顯示的 FPS
應該是 60
左右, 使用 1/30
來試驗, 則顯示 FPS
爲 30
左右, 使用 1/2
來試驗, 則 FPS
爲 2
左右, 看來這個嘗試成功了!
後續咱們要在這個基礎上搞一些更有趣的代碼出來.