用 Lua 的協程 coroutine 控制 Codea 屏幕刷新速度

用 Lua 的協程 coroutine 控制 Codea 屏幕刷新速度

概述

Codea 中, 函數 draw() 缺省每秒執行 60 次, 咱們但願能修改一下它的刷新速度, 因而想到了 Lua 的一個特性:協程 coroutine, 但願試着用它來控制程序執行的節奏, 不過目前對於協程還不太瞭解, 那就一邊看教程, 一邊試驗好了.多線程

Codea 運行機制

咱們知道, Codea 的運行機制是這樣的:框架

  • setup() 只在程序啓動時執行一次
  • draw() 在程序執行完 setup() 後反覆循環執行, 每秒執行 60
  • touched()draw() 相似, 也是反覆循環執行

簡單說, 就是相似於這樣的一個程序結構:函數

setup()

while true do
	...
	draw()
	touched(touch)
	...
end

協程 coroutine 的簡單介紹

Lua 所支持的協程全稱被稱做協同式多線程collaborative multithreading)。Lua爲每一個 coroutine 提供一個獨立的運行線路。然而和多線程不一樣的地方就是,coroutine 只有在顯式調用 yield 函數後才被掛起,再調用 resume 函數後恢復運行, 同一時間內只有一個協程正在運行。.net

Lua 將它的協程函數都放進了 coroutine 這個表裏,其中主要的函數以下:線程

表格圖

協程 coroutine 的使用示例

新建協程 coroutine.create()

使用 coroutine.create(f) 能夠爲指定函數 f 新建一個協程 co, 代碼以下:code

-- 先定義一個函數 f
function f ()
	print(os.time())
end

-- 爲這個函數新建一個協程
co = coroutine.create(f)

一般協程的例子都是直接在 coroutine.create() 中使用一個匿名函數做爲參數, 咱們這裏爲了更容易理解, 專門定義了一個函數 f.orm

  • 爲一個函數新建協程的意義就在於咱們能夠經過協程來調用函數.

爲何要經過協程來調用函數呢? 由於若是咱們直接調用函數, 那麼從函數開始運行的那一刻起, 咱們就只能被動地等待函數裏的語句徹底執行完後返回, 不然是沒辦法讓函數在運行中暫停/恢復, 而若是是經過協程來調用的函數, 那麼咱們不只可讓函數暫停在它內部的任意一條語句處, 還可讓函數隨時從這個位置恢復運行.協程

也就是說, 經過爲一個函數新建協程, 咱們對函數的控制粒度從函數級別精細到了語句級別.blog

協程狀態 coroutine.status()

咱們能夠用 coroutine.status(co) 來查看當前協程 co 的狀態教程

> coroutine.status(co)
suspended
>

看來新建的協程默認是被設置爲 掛起-suspended 狀態的, 須要手動恢復.

恢復協程 coroutine.resume()

執行 coroutine.resume(co), 代碼以下:

> coroutine.resume(co)
1465905122
true
>

咱們再查看一下協程的狀態:

> coroutine.status(co)
dead
>

顯示已經死掉了, 也就是說函數 f 已經執行完了.

掛起協程 coroutine.yield()

有人就問了, 這個例子一會兒就執行完了, 協程只是在最初被掛起了一次, 咱們如何去手動控制它的掛起/恢復呢? 其實這個例子有些太簡單, 沒有很好地模擬出適合協程發揮做用的使用場景來, 設想一下, 咱們有一個函數執行起來要花不少時間, 若是不使用協程的話, 咱們就只能傻傻地等待它執行完.

用了協程, 咱們就能夠在這個函數執行一段時間後, 執行一次 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 裏去了.

協程 coroutine 跟 Codea 代碼框架的結合

上面那個例程中, 設置的時間片越小, 程序的控制權切換得越頻繁, 這一點剛好能夠用來設置 Codea 的屏幕刷新速度.

首先把那些只運行一次的函數和語句放到 setup() 中, 其次把那些須要反覆執行的函數和語句放到 draw() 中, 這裏須要稍做修改, 由於 Codeadraw() 自然地就是一個大循環, 因此咱們能夠考慮把 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來試驗, 則顯示 FPS30 左右, 使用 1/2 來試驗, 則 FPS2 左右, 看來這個嘗試成功了!

後續咱們要在這個基礎上搞一些更有趣的代碼出來.

參考

快速掌握Lua 5.3 —— Coroutines
【深刻Lua】理解Lua中最強大的特性-coroutine(協程)

相關文章
相關標籤/搜索