在 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
左右, 看来这个尝试成功了!
后续咱们要在这个基础上搞一些更有趣的代码出来.