openluat的三种程序调度方式(定时器、协程、注册)

这是最近使用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") -- 响应两次

关于这点的原理可以查看库函数文件 systimerStart函数,如下所示:

--- 开启一个定时器
-- @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,在请求进入函数之后会进行以下步骤:

  1. 如果没有信号量,则创建一个信号量。
  2. 查询队列内是否有相同id的定时器,如果有则清除该定时器。
  3. 等待信号量释放。
  4. 查询队列是否有空位。
  5. 如果没有则弹一个TRACE,并跳到error处理。
  6. 创建一个定时器,并设置回调函数。
  7. 各种查询,如果失败则调到error处理。
  8. 释放信号量。
  9. 返回设置成功。

关于定时器超时检测是怎么在底层实现的,暂时不打算寻找了,估计也是 Tick 中断+优先级队列吧,之后有空在继续研究它了。

其实lua的源码并不复杂,只是回调和宏定义稍微多了一些,变量和函数的命名缩写了一些,但是想要搞懂也只是时间问题,有空开个坑写一下《阿绪的lua学习笔记》吧。

协程

其实协程不是lua的概念,它在计算机诞生的时刻就出现了,但是因为一些原因,后面慢慢被线程和进程取代了一部分功能,可以把它简单的理解成用户空间下的线程。需要注意的是,协程需要用户主动挂起,不会有超时管理,也无法实现并发处理和高实时的响应,当然官方是这么说的(虽然确是是这样的):

下面是一个用协程打印hello的例子:

function Hello()
    while true do
        print("hello")
        sys.wait(1000)
    end
end

sys.taskInit(Hello)

关于协程openluat官方的资料也是蛮多的,基本上看wiki就够了,阿绪这边就大概就总结这么几点。

  1. 在协程中需要使用死循环,不然运行完成就退出任务了。
  2. 在协程中,必须及时挂起,不然会饿死其他协程。
  3. 在协程中,要使用wait做延时,不要使用定时器。
  4. 任务是对协程的封装。

其中,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 国际许可协议进行许可。
10月 ago

发表评论