• 欢迎大家分享资料!前往留言板评论即可!

Luat task 延时那些事

合宙 模组资料网 2年前 (2021-05-15) 447次浏览 0个评论 扫描二维码

我们知道, 在 lua 中是不存在多线程的, 只是实现了一个相对轻量级的协程用来满足对多线任务的一些需求, lua 使用 Clean C (标准C和标准C++的公共子集) 实现, 然而 Clean C 中并没有直接提供有关多线程的实现, 多线程其实更依赖于其所运行的系统, 出于轻量以及移植性方面的考虑, lua 放弃了对多线程的实现, 利用协程进行替代.

我们首先来看下协程的简单使用:

function A()
    for i=1, 5 do
        coroutine.resume(co)
        print("A\t"..i) 
    end 
end

function B()
    for i=1, 5 do
        coroutine.yield()
        print("B\t"..i)
    end
end

co = coroutine.create(B)

A()

--[[
    A   1
    B   1
    A   2
    B   2
    A   3
    B   3
    A   4
    B   4
    A   5
]]

这是一种简单的使用场景, yield 和 resume 只是负责切换控制, 让控制权在两个任务之间来回切换, 达到了使两个任务 “并行” 的目的. 当然 yield 和 resume 之间还可以传入和返回参数, 所以两个协程之间也并非处于完全相等的地位, 他们的主从关系还是有一些细微差别的, 不过这个以后有时间再讲. 协程只是对多线程的一种模拟, 并不能替代多线程. 它把本该在一个地方实现的代码拆分到了不同任务, 这会让逻辑表述看起来更加清晰. 了解了基本使用之后, 我们现在来看个需求, 还是上面的代码, 不过我们这回希望 A 和 B 能分别以 1000s 的延时间隔打印, 那么代码该如何实现? 听完之后, 你可能写出下面的代码:

“`lua
function wait(ms)
— 阻塞延时, 仅仅只做说明, 并不实现
end

function A()
for i=1, 5 do
coroutine.resume(co)
print("A\t"..i)
wait(1000)
end
end

function B()
for i=1, 5 do
coroutine.yield()
print("B\t"..i)
wait(1000)
end
end

co = coroutine.create(B)

A()

<pre><code class="line-numbers">我们期望两个协程都能以 1000ms 的延时打印输出, 但是这种阻塞延时其实是会在两个协程之间相互影响的, A 在延时的过程中其实是会加长 B 的延时, 最终两个协程都会以 2000ms 的延时打印输出, 当然, 聪明的你可会想到让每个协程延时 500ms 来解决上面问题, 但是如果是多个协程分别以不同的时间进行延时, 又该如何实现呢? 说到这里, 就不得不提到非阻塞延时的概念了, 实现效果就是, 延时这种操作不再影响全局, 只对当前协程有效, 下面我们就来看看这种非阻塞延时的机制是如何实现的.

虽然在 lua 层也可以, 但是 luat 的非阻塞延时其实是靠 Core 层的 RTT 实现的, RTT 的全称是 Real Time-Thread, 是一个实时多线程操作系统, 主要用于嵌入式, 基本属性之一就是支持多任务, 也是个轻量级的实现, 可以很方便的裁剪, 定制, 由国人开发维护. luat 的 task 框架主要用到了其中的定时器进行延时, 所以我们先来看看 RTT 的定时器是怎么运行的. 在单任务系统, 依靠切换控制权来模拟多线程的话, 那延时必定不可能是真正"延时"的, 一个任务的阻塞延时肯定会干扰到其他任务的计时, 所以一定会有一个第三者来进行时间管理, 我们可以称之为时钟调度器, Core 的时钟调度器自己维护了一个时钟和一个时间管理链表, 时钟的最小计时单位是时钟节拍, 所以延时时间只能是时钟节拍的整数倍.
“`lua
— 伪代码, 仅作原理说明
— 任务A
task(function()
while true do
wait(100)
print(“A”)
end
end)

— 任务B
task(function()
while true do
wait(80)
print(“B”)
end
end)

run()

假设现在时钟节拍是 1ms, 任务 A 需要间隔 100ms 执行, 任务 B 需要间隔 80ms 执行, 那么运行情况将会是这样的, 最开始控制权是在 Core 手中, 当完成一些列初始化工作后, Core 的时钟到了 30ms (假设初始工作需要 30ms), 此时 Core 会把控制权交给任务A执行. 

Luat task 延时那些事

任务 A 在执行到 wait(100) 时会把当前延时时间加上系统时钟时间的数值, 连同自身ID, 添加进 Core 的时间链表.

Luat task 延时那些事

然后来到了任务 B, 任务 B 在执行到 wait(80) 时也会向 Core 中添加消息

Luat task 延时那些事

接着会执行到 run, run 开始从 Core 中阻塞读取消息

Luat task 延时那些事

之后 Core 会以 1ms 的间隔独自计时, 每过 1ms, Core 都会检查链表第一项时间是否达到, 当时钟计时达到时间的时候, 控制权会交还给 run, 并且告诉 run, B 的计时时间到了

Luat task 延时那些事
之后 run 会把控制权交给 B, B 执行完返还控制权, 当然 B 在执行过程中依然会向 Core 中添加消息,

Luat task 延时那些事

再然后 run 会接着阻塞读取, 如此往复…

Luat task 延时那些事

最开始从 Core–>A–>B–>run 的控制权移交顺序是由代码顺序决定的, 因为 Core 会先启动, 然后执行 lua 脚本, 脚本中代码顺序正好是 A, B, run, 再之后的顺序才是调度框架的顺序. 可以看到, 整个框架的核心就是 run, run 会从 Core 中阻塞读取消息, 然后依照消息编号调用其他任务, 其他任务执行完, 或者执行到延时时, 会向 run 返还控制权, 由 run 接着调度, 没有显式调用的话, 其他任务之间是不会相互移交控制权的. run 和其他任务之间的控制权移交是依靠协程实现的, 其他任务和 Core 的交互仅仅只是向时间链表中添加消息. 之所以用链表实现是为了方便添加与移除消息, 在添加时 Core 还会对时间进行排序, 这样只需比较链表的首项就能完成判断, 不需要对所有时间都进行逐一比较.
实际上 luat 在设定时间到达后并不是通过直接调用方式传递控制权, 而是用消息队列的方式把控制权交回到了lua 层, 这是 RTT 的另一项机制, 因为除了时钟消息, Core 层还有其他消息需要传递给lua. 通过上面的分析我们也不难发现, lua 层当中的所有代码几乎都是瞬时完成的, 所有延时操作都是把控制权移交到了 Core. 以上就是关于 luat task 延时的一些事.

参考:

LuatOS https://github.com/openLuat/LuatOS

RT-Thread 文档中心 https://www.rt-thread.org/document/site/


转载请注明原文链接:Luat task 延时那些事
喜欢 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址