这是最近使用Air202开发时遇到的一个问题,想来也是不少从单片开发转来做lua开发的人会遇到的问题:在lua中该怎么调度我的程序,协程和定时器又有什么同异?在这里阿绪就把最近开发的一些心得和体会总结出来,如果有讲错的地方还请大家指教。
在 openluat 的官方wiki中指出,luat的程序有四种运行方式,直接调用、协程、定时器、程序注册。其中直接调用和普通单片开发一致,故不这里在赘述了。
定时器
定时器是lua中和单片机裸机开发最像的一个,在 openluat 中提供了两个函数 sys.timerLoopStart(fnc, ms, ...)
和 sys.timerStart(fnc, ms, ...)
它们的区别就是,前者是循环调用,后者是一次性调用。下面放两个例子,它们两个实现的结果是一致的。
function Hello() print("hello") sys.timerStart(Hello,1000) end Hello() sys.timerLoopStart(function() print("hello") end,1000)
关于 openluat 中定时器还有一点可以注意的就是,一个函数设置两个定时器,如入参一样则后者覆盖前者,如入参不一样则记录两次。
function Hello() print("hello") end sys.timerStart(Hello,1000) sys.timerStart(Hello,2000) -- 覆盖上一个定时器
function Hello(s) print(s) end sys.timerStart(Hello,1000,"hello") sys.timerStart(Hello,2000,"world") -- 响应两次
关于这点的原理可以查看库函数文件 sys 的timerStart函数,如下所示:
--- 开启一个定时器 -- @param fnc 定时器回调函数 -- @number ms 整数,最大定时126322567毫秒 -- @param ... 可变参数 fnc的参数 -- @return number 定时器ID,如果失败,返回nil function timerStart(fnc, ms, ...) --回调函数和时长检测 assert(fnc ~= nil, "sys.timerStart(first param) is nil !") assert(ms > 0, "sys.timerStart(Second parameter) is <= zero !") -- 关闭完全相同的定时器 if arg.n == 0 then timerStop(fnc) else timerStop(fnc, unpack(arg)) end -- 为定时器申请ID,ID值 1-20 留给任务,20-30留给消息专用定时器 while true do if msgId >= MSG_TIMER_ID_MAX then msgId = TASK_TIMER_ID_MAX end msgId = msgId + 1 if timerPool[msgId] == nil then timerPool[msgId] = fnc break end end --调用底层接口启动定时器 if rtos.timer_start(msgId, ms) ~= 1 then log.debug("rtos.timer_start error") return end --如果存在可变参数,在定时器参数表中保存参数 if arg.n ~= 0 then para[msgId] = arg end --返回定时器id return msgId end
上面是 LuaTask 中 timerStart 的完成函数。因为源码中注释十分完善,这边就不重复了,无非就是查重、请求、设置、返回这几步。其中最重要的莫过于 rtos.timer_start(msgId, ms)
这句。在查询源码之后可以发现,该接口在底层申明为 l_rtos_timer_start
函数,而其又调用了platform_rtos_start_timer
。
static int l_rtos_timer_start(lua_State *L) { int timer_id = luaL_checkinteger(L,1); int ms = luaL_checkinteger(L,2); int ret; ret = platform_rtos_start_timer(timer_id, ms,FALSE); lua_pushinteger(L, ret); return 1; }
int platform_rtos_start_timer(int timer_id, int milliSecond, BOOL high) { u8 index; HANDLE hTimer = OPENAT_INVALID_HANDLE; if(!hLuaTimerSem) { hLuaTimerSem = IVTBL(create_semaphore)(1); } for(index = 0; index < MAX_LUA_TIMERS; index++) { if(luaTimerParam[index].hOsTimer != OPENAT_INVALID_HANDLE && luaTimerParam[index].luaTimerId == timer_id) { platform_rtos_stop_timer(timer_id); break; } } IVTBL(wait_semaphore)(hLuaTimerSem, 0); for(index = 0; index < MAX_LUA_TIMERS; index++) { if(luaTimerParam[index].hOsTimer == NULL) { break; } } if(index >= MAX_LUA_TIMERS) { DEBUG_RTOS_TRACE("[platform_rtos_start_timer]: no timer resource."); goto start_timer_error; } hTimer = IVTBL(create_timer)(high ? platform_rtos_timer_high_priority_callback : platform_rtos_timer_callback, (PVOID)timer_id); if(OPENAT_INVALID_HANDLE == hTimer) { DEBUG_RTOS_TRACE("[platform_rtos_start_timer]: create timer failed."); goto start_timer_error; } if(!IVTBL(start_timer)(hTimer, milliSecond)) { DEBUG_RTOS_TRACE("[platform_rtos_start_timer]: start timer failed."); goto start_timer_error; } luaTimerParam[index].hOsTimer = hTimer; luaTimerParam[index].luaTimerId = timer_id; IVTBL(release_semaphore)(hLuaTimerSem); return PLATFORM_OK; start_timer_error: IVTBL(release_semaphore)(hLuaTimerSem); /*+\NEW\liweiqiang\2014.7.22\修正启动定时器失败把定时器资源耗尽的问题 */ if(OPENAT_INVALID_HANDLE != hTimer) /*-\NEW\liweiqiang\2014.7.22\修正启动定时器失败把定时器资源耗尽的问题 */ { IVTBL(delete_timer)(hTimer); } return PLATFORM_ERR; }
因为源码十分直观,这边就大概的解释一下。在 openluat 的底层,存放了一个结构体数组做的队列——luaTimerParam,在请求进入函数之后会进行以下步骤:
- 如果没有信号量,则创建一个信号量。
- 查询队列内是否有相同id的定时器,如果有则清除该定时器。
- 等待信号量释放。
- 查询队列是否有空位。
- 如果没有则弹一个TRACE,并跳到error处理。
- 创建一个定时器,并设置回调函数。
- 各种查询,如果失败则调到error处理。
- 释放信号量。
- 返回设置成功。
关于定时器超时检测是怎么在底层实现的,暂时不打算寻找了,估计也是 Tick 中断+优先级队列吧,之后有空在继续研究它了。
其实lua的源码并不复杂,只是回调和宏定义稍微多了一些,变量和函数的命名缩写了一些,但是想要搞懂也只是时间问题,有空开个坑写一下《阿绪的lua学习笔记》吧。
协程
其实协程不是lua的概念,它在计算机诞生的时刻就出现了,但是因为一些原因,后面慢慢被线程和进程取代了一部分功能,可以把它简单的理解成用户空间下的线程。需要注意的是,协程需要用户主动挂起,不会有超时管理,也无法实现并发处理和高实时的响应,当然官方是这么说的(虽然确是是这样的):

下面是一个用协程打印hello的例子:
function Hello()
while true do
print("hello")
sys.wait(1000)
end
end
sys.taskInit(Hello)
关于协程openluat官方的资料也是蛮多的,基本上看wiki就够了,阿绪这边就大概就总结这么几点。
- 在协程中需要使用死循环,不然运行完成就退出任务了。
- 在协程中,必须及时挂起,不然会饿死其他协程。
- 在协程中,要使用wait做延时,不要使用定时器。
- 任务是对协程的封装。
其中,wait函数的实现方式如下所示:
--- Task任务延时函数,只能用于任务函数中 -- @number ms 整数,最大等待126322567毫秒 -- @return 定时结束返回nil,被其他线程唤起返回调用线程传入的参数 -- @usage sys.wait(30) function wait(ms) -- 参数检测,参数不能为负值 assert(ms > 0, "The wait time cannot be negative!") -- 选一个未使用的定时器ID给该任务线程 if taskTimerId >= TASK_TIMER_ID_MAX then taskTimerId = 0 end taskTimerId = taskTimerId + 1 local timerid = taskTimerId taskTimerPool[coroutine.running()] = timerid timerPool[timerid] = coroutine.running() -- 调用core的rtos定时器 if 1 ~= rtos.timer_start(timerid, ms) then log.debug("rtos.timer_start error") return end -- 挂起调用的任务线程 local message = {coroutine.yield()} if #message ~= 0 then rtos.timer_stop(timerid) taskTimerPool[coroutine.running()] = nil timerPool[timerid] = nil return unpack(message) end end
和 timerStart 相比,最大的区别就是多了一个 taskTimerPool 表和设置完定时器之后使用 coroutine.yield() 挂起协程,等待唤醒。
注册
程序注册类似实时操作系统里的邮件,无非就是发布(生产者)和订阅(消费者)。
注册可以在任务中使用,也可以直接使用,反正不要在定时器中使用就行(应该也没人这么用吧)。感觉其它没啥好讲的,就扔两个官方的demo吧。
--testMsgPub.lua module(...,package.seeall) require"sys" local a = 2 local function pub() print("pub") sys.publish("TEST",a) --可以发布多个变量sys.publish("TEST",1,2,3) end pub() --testMsgSub.lua module(...,package.seeall) require"sys" local function subCallBack(...) print("rev",arg[1]) end sys.subscribe("TEST",subCallBack)
在任务中使用订阅:
--testMsgSub.lua module(...,package.seeall) require"sys" local a = 2 local function pub() print("pub") sys.publish("TEST",a) end pub() sys.taskInit(function() while true do result, data = sys.waitUntil("TEST", 10000) if result == true then print("rev") print(data) end sys.wait(2000) end end)
主调度框架
local function safeRun() -- 分发内部消息 dispatch() -- 阻塞读取外部消息 local msg, param, exparam = rtos.receive(rtos.INF_TIMEOUT) -- 判断是否为定时器消息,并且消息是否注册 if msg == rtos.MSG_TIMER and timerPool[param] then if param < TASK_TIMER_ID_MAX then local taskId = timerPool[param] timerPool[param] = nil if taskTimerPool[taskId] == param then taskTimerPool[taskId] = nil coroutine.resume(taskId) end else local cb = timerPool[param] --如果不是循环定时器,从定时器id表中删除此定时器 if not loop[param] then timerPool[param] = nil end if para[param] ~= nil then cb(unpack(para[param])) if not loop[param] then para[param] = nil end else cb() end --如果是循环定时器,继续启动此定时器 if loop[param] then rtos.timer_start(param, loop[param]) end end --其他消息(音频消息、充电管理消息、按键消息等) elseif type(msg) == "number" then handlers[msg](param, exparam) else handlers[msg.id](msg) end end
上面是openluat的主调度框架,官方的注释还是很明确的,就不过多的解释了。
参考资料
https://ask.openluat.com/article/14
https://ask.openluat.com/article/81
https://www.zhihu.com/question/20511233
https://wiki.openluat.com/doc/luatFramework/#_6
https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272

本作品采用知识共享署名 4.0 国际许可协议进行许可。