用 Lua 的协程 coroutine 在 Codea 上实现一个多任务调度类
概述
问题描述
在 Codea
中调试程序时发现一个问题: 如果在 setup()
中执行了比较耗时的语句, 比如地图生成, 资源下载等操作, 那么在该这些操作没有完成之前屏幕上是不会显示任何内容的, 你只能傻傻地等它完成, 如果是调试版本还可以通过 print
在侧面的调试窗口打印一些信息, 如果是正式版本就不太适合调出调试窗口了, 怎么办呢?
解决办法
于是一边学习研究 Lua
的协程 coroutine
, 一边写了一个多任务调度类, 该类有如下特点特点:
- 可自由设置全局性的时间片
- 可针对每个任务设置不同的时间片(理论上可行, 尚未实际验证)
- 可自由添加不同任务
也可以把它看做一个线程类, 用它来控制流程就可以解决上面遇到的问题.
多任务调度类
类代码
类代码如下:
--# Threads
-- 最新版本, 可自由增加多个不同任务
Threads = class()
function Threads:init()
self.threads = {}
self.taskList = {}
self.time = os.clock()
self.timeTick = 0.01
self.taskID = 1
self.taskStatus = {}
self.taskVT = {}
self.img = image(100,100)
end
-- 设置任务函数,插入任务列表
function Threads:addTaskToList(task)
local t = function() task() end
table.insert(self.taskList, t)
end
-- 为所有任务创建对应的协程,该函数执行一次即可。
function Threads:job()
-- 为任务列表中的所有任务函数,都创建对应的协程,并插入 self.threads 表中
local n = #self.taskList
for id = 1, n do
-- local f = function () self.taskList[id]() end
local f = function () self:taskUnit(id) end
-- 为 taskUnit() 函数创建协程。
local co = coroutine.create(f)
table.insert(self.threads, co)
-- 记录所有任务的状态,此时应为 suspended
self.taskStatus[id] = coroutine.status(co)
end
end
-- 任务单元,要在本函数中设置好挂起条件
function Threads:taskUnit(id)
-- 可在此处执行用户的任务函数
-- self.task()
self.taskID = id
self.taskList[id]()
-- 切换点, 放在 self.task() 函数内部耗时较长的位置处, 以方便暂停
-- self:switchPoint(id)
-- 运行到此说明任务全部完成, 设置状态
-- self.taskStatus[id] = "Finished"
end
-- 切换点, 可放在准备暂停的函数内部, 一般选择放在多重循环的最里层, 这里耗时最多
function Threads:switchPoint(id)
-- 切换线程,时间片耗尽,而工作还没有完成,挂起本线程,自动保存现场。
if (os.clock() - self.time) >= self.timeTick then
-- 查看调试信息,尽量放在这里,尤其是 print 函数,不要放在任务函数内部
print("hello: No."..id.." is "..self.taskStatus[id])
-- self:visual(id)
-- 重置任务时间
self.time = os.clock()
-- 挂起当前协程
coroutine.yield()
end
end
function Threads:visual(id)
local n = #self.taskList
local vt = {}
background(18, 16, 16, 255)
setContext(self.img)
pushStyle()
strokeWidth(1)
fill(255, 211, 0, 255)
-- if self.taskID == 1 then fill(241, 7, 7, 255) else fill(255, 211, 0, 255) end
local w,h = self.img.width/n, self.img.height/n
local x,y = 0,0
for i = 1, n do
vt[i] = function () rect(100+x+(i-1)*w,100+y+(i-1)*h,w,h) end
end
popStyle()
setContext()
-- sprite(self.img,300,300)
-- vt[self.taskID]()
print("id: "..id)
vt[id]()
end
-- 在 draw 中运行的分发器,借用 draw 的循环运行机制,调度所有线程的运行。
function Threads:dispatch()
local n = #self.threads
-- 线程表空了, 表示没有线程需要工作了。
if n == 0 then return end
for i = 1, n do
-- 记录哪个线程在工作。
self.taskID = i
-- 恢复"coroutine"工作。
local status = coroutine.resume(self.threads[i])
-- 记录任务状态
self.taskStatus[i] = coroutine.status(self.threads[i])
-- 线程是否完成了他的工作?"coroutine"完成任务时,status是"false"。
-- 若完成则将该线程从调度表中删除, 将对应任务从任务列表删除,同时返回。
if not status then
self.taskStatus[i] = "Finished"
table.remove(self.threads, i)
-- table.remove(self.taskList,i)
return
end
end
end
```
具体代码就不多解释了, 基本上每行都有注释.
### 测试代码
可用如下的主程序框架来测试:
-
主程序框架 function setup() print("thread...")
myT = Threads() myT.timeTick = 1/2 myT:addTaskToList(tt) myT:addTaskToList(oo) myT:addTaskToList(mf) myT:addTaskToList(pk)
--[[ myT.taskList2 --]]
myT:job()
print(unpack(myT.taskList))
end
function draw() background(0) -- sprite("Documents:3D-Wall", WIDTH/2,HEIGHT/2) myT:dispatch() fill(244, 27, 27, 255) print("2: "..myT.taskStatus[1]) print("length: "..#myT.taskList) -- local per = string.format("Worker %d calculating, %f%%.", p, (k / to * 100)) -- text(per,300,300) sysInfo() end
function tt () while true do -- print("tt: "..os.clock()) myT:switchPoint(myT.taskID) end end
function oo () while true do -- print("oo: "..os.clock()) myT:switchPoint(myT.taskID) end end
function mf () local k = 0 for i=1,10000000 do k = k + i -- print("mf: "..k) -- 如果运行时间超过 timeTick 秒, 则暂停 myT:switchPoint(myT.taskID) end end
function pk () local k = 0 for i=1,10000000 do k = k + i -- print("pk: "..k) -- 如果运行时间超过 timeTick 秒, 则暂停 myT:switchPoint(myT.taskID) end end
### 应用场景
- 场景1
在 `setup()` 执行比较耗时的函数时, 可以暂停挂起该函数, 跳转到 `draw()` 往屏幕上输出一些提示信息, 具体做法就是把该函数作为任务加入线程类的任务列表, 然后在该函数最耗时的代码位置处插入 `switchPoint()` 函数, 设置好时间片.
- 场景2
执行一些 `http.request` 或 `socket` 操作时, 为避免长时间等待, 也可以把这些操作作为任务加入线程类的任务列表, 然后在该函数最耗时的代码位置处插入 `switchPoint()` 函数, 设置好时间片,
- 场景3
需要轮流执行多个任务时, 可以把所有任务都加入任务列表, 用它来调度.
总之就是诸如此类的情况都可以使用.