用 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
函数后恢复运行, 同一时间内只有一个协程正在运行。
Lua
将它的协程函数都放进了 coroutine
这个表里,其中主要的函数如下:
协程 coroutine 的使用示例
新建协程 coroutine.create()
使用 coroutine.create(f)
可以为指定函数 f
新建一个协程 co
, 代码如下:
-- 先定义一个函数 f
function f ()
print(os.time())
end
-- 为这个函数新建一个协程
co = coroutine.create(f)
通常协程的例子都是直接在 coroutine.create()
中使用一个匿名函数作为参数, 我们这里为了更容易理解, 专门定义了一个函数 f
.
- 为一个函数新建协程的意义就在于我们可以通过协程来调用函数.
为什么要通过协程来调用函数呢? 因为如果我们直接调用函数, 那么从函数开始运行的那一刻起, 我们就只能被动地等待函数里的语句完全执行完后返回, 否则是没办法让函数在运行中暂停/恢复
, 而如果是通过协程来调用的函数, 那么我们不仅可以让函数暂停在它内部的任意一条语句处, 还可以让函数随时从这个位置恢复运行.
也就是说, 通过为一个函数新建协程, 我们对函数的控制粒度从函数级别精细到了语句级别.
协程状态 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()
中, 这里需要稍作修改, 因为 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
左右, 看来这个尝试成功了!
后续我们要在这个基础上搞一些更有趣的代码出来.
参考
快速掌握Lua 5.3 —— Coroutines
【深入Lua】理解Lua中最强大的特性-coroutine(协程)