在luatos中实现互斥锁的功能-以合宙luatos和air780EGH为实现

经常使用freertos进行开发的用户都会经常使用互斥锁的功能,主要是用于实现原子操作,避免冲突,但是在合宙的luatos上,其实没有相关的api接口,lua本身也不支持这样操作,于是本人研究了一下,写出了一个库用于实现互斥锁的功能,可能不太完善,欢迎交流

实现原理

有人会说,lua本身就是协程式的多仍任务,要锁做什么,是这样的,确实也不需要,但是复杂任务的时候,比如串口抢占,websocket抢占,都涉及到先后操作和逻辑竞争,这个时候就需要锁了,而要实现互斥锁,我们需要两个核心的api sys.waitUntilsys.publish


  • sys.waitUntil(msg, timeout) :对应 RTOS 的 Block (阻塞等待)。它会让当前协程挂起,直到收到指定的消息或超时。
  • sys.publish(msg) :对应 RTOS 的 Unblock (唤醒)。它会广播一个消息,唤醒所有正在等待该消息的协程。

我们可以在此基础上,封装成类实现互斥锁

实现代码

lua 复制代码
-- lib_mutex.lua

local Mutex = {}
Mutex.__index = Mutex

-- 创建一个新的互斥锁
function Mutex.new()
    local o = {
        locked = false,       -- 锁状态
        owner = nil,          -- 持有者
        id = tostring(os.time()) .. tostring(math.random()) -- 生成唯一等待ID
    }
    return setmetatable(o, Mutex)
end

-- 获取锁 (类似 xSemaphoreTake)
-- timeout: 超时时间(ms),默认为一直等待
-- 返回值: true 成功获取, false 超时失败
function Mutex:lock(timeout)
    local wait_result
    -- 如果锁已经被占用了,就挂起当前协程等待
    if timeout and type(timeout) == "number" then
        timeout = math.floor(timeout)
    end
    while self.locked do
        -- 当前协程会停在这里,直到有人调用 unlock 触发消息,或者超时
        wait_result = sys.waitUntil(self.id, timeout)
        
        -- 如果是因为超时醒来的 (wait_result 为 nil),则抢锁失败
        if wait_result == nil then
            return false
        end
        -- 如果是被唤醒的,循环会继续,再次检查 self.locked
    end

    -- 成功抢到锁
    self.locked = true
    self.owner = coroutine.running() -- 记录是谁拿了锁
    return true
end

-- 释放锁 (类似 xSemaphoreGive)
function Mutex:unlock()
    -- 只有锁的持有者才能释放锁 (防止误操作,可选)
    if self.owner ~= coroutine.running() then return end 
    
    if self.locked then
        self.locked = false
        self.owner = nil
        -- 醒来后会重新进入 while 循环抢锁,由于 Lua 是单线程,必定有一个会先抢到,其他的继续睡
        sys.publish(self.id) 
    end
end

return Mutex

代码解释(很罗嗦可跳过)

首先看数据结构的设计,在 Mutex.new 构造函数中,我们定义了锁的三个元数据:locked 状态位用于原子性地标记资源是否被占用;owner 用于记录持有当前锁的协程句柄,这至关重要,它确保了"谁加锁谁解锁"的安全原则,防止其他任务意外释放不属于自己的锁;而 id 则是一个由时间戳和随机数生成的唯一字符串,它作为消息通道的唯一标识符,保证了系统内不同锁对象之间的事件广播互不干扰。

接下来是至关重要的上锁逻辑 Mutex:lock。该函数首先对传入的超时参数进行了整数化处理(math.floor),这是为了防止浮点数导致底层定时器崩溃。紧接着进入核心的竞争循环,这里使用 while self.locked 循环而不是简单的 if 判断,是因为在多任务环境下,当锁被释放并广播消息时,可能有多个等待中的协程同时被唤醒,但由于 Lua 是单线程执行的,它们只能依次运行。假设任务 A 和 B 同时被唤醒,A 先运行发现锁空闲并抢占了它,当 B 随后运行时,必须能再次检测到锁已被 A 抢占(locked 变回 true),从而再次进入 sys.waitUntil 挂起等待。当 waitUntil 返回代表超时的结果时直接返回 false,避免任务无限期死等。一旦成功跳出循环,函数立即标记 locked 为 true 并记录当前协程为 owner,由于 Lua 协程的非抢占特性,这两步操作中间不会被打断,从而保证了加锁过程的原子性。

最后是解锁逻辑 Mutex:unlock。为了保证业务逻辑的安全性,代码首先通过 coroutine.running() 校验当前协程是否为锁的持有者,有点类似递归锁或二值信号量的所有权保护,防止非持有者误操作破坏业务逻辑。校验通过后,将 locked 状态重置为 false 并清空 owner,随即调用 sys.publish(self.id) 广播消息。这一步是整个机制的触发器,它通知 LuatOS 调度器查找所有正在该 id 上阻塞等待的协程,将它们的状态从"挂起"转为"就绪",使它们在下一个调度周期有机会重新进入 lock 函数的循环中竞争资源。

测试代码

lua 复制代码
local Mutex = require "mutex"

-- 创建一个全局锁,保护"扬声器"资源
local speaker_lock = Mutex.new()

-- 模拟 TTS 播放函数
local function speak(text)
    log.info("TTS", "准备播放:", text)
    
    -- 尝试拿锁,等待时间无限
    if speaker_lock:lock() then
        log.info("LOCK", text,"拿到锁了,开始播放...")
        
        -- 模拟播放过程耗时 2秒
        sys.wait(2000) 
        log.info("TTS", "播放结束:", text)
        
        -- 释放锁
        speaker_lock:unlock()
        log.info("LOCK", "锁已释放")
    else
        log.info("LOCK", "获取锁超时/失败")
    end
end
log.info("TTS","拿锁比赛现在开始")
-- 任务 A:每 3 秒想说话
sys.taskInit(function()
    while true do
        speak("我是任务 A")
        sys.wait(3000)
    end
end)

-- 任务 B:每 1 秒想说话
sys.taskInit(function()
    sys.wait(500) 
    while true do
        speak("我是任务 B")
        sys.wait(1000)
    end
end)

sys.run()

下面式运行日志

txt 复制代码
[2025-12-17 17:57:32.702][000000000.232] D/main loadlibs psram 3211632 104972 106200
[2025-12-17 17:57:32.705][000000000.251] I/user.main Air780EGH_gnss 1.0.0
[2025-12-17 17:57:32.708][000000000.252] I/user.main Air780EGH_gnss
[2025-12-17 17:57:32.711][000000000.480] I/user.TTS 拿锁比赛现在开始
[2025-12-17 17:57:32.713][000000000.481] I/user.TTS 准备播放: 我是任务 A
[2025-12-17 17:57:32.716][000000000.481] I/user.LOCK 我是任务 A 拿到锁了,开始播放...
[2025-12-17 17:57:32.898][000000000.992] I/user.TTS 准备播放: 我是任务 B
[2025-12-17 17:57:33.944][000000002.006] D/mobile cid1, state0
[2025-12-17 17:57:33.950][000000002.006] D/mobile bearer act 0, result 0
[2025-12-17 17:57:33.953][000000002.007] D/mobile NETIF_LINK_ON -> IP_READY
[2025-12-17 17:57:33.970][000000002.063] D/mobile TIME_SYNC 0
[2025-12-17 17:57:34.266][000000002.358] soc_cms_proc 2219:cenc report 1,51,1,15
[2025-12-17 17:57:34.376][000000002.463] D/mobile NETIF_LINK_ON -> IP_READY
[2025-12-17 17:57:34.390][000000002.481] I/user.TTS 播放结束: 我是任务 A
[2025-12-17 17:57:34.393][000000002.482] I/user.LOCK 锁已释放
[2025-12-17 17:57:34.395][000000002.483] I/user.LOCK 我是任务 B 拿到锁了,开始播放...
[2025-12-17 17:57:35.526][000000003.628] D/mobile ims reg state 0
[2025-12-17 17:57:35.529][000000003.629] D/mobile LUAT_MOBILE_EVENT_CC status 0
[2025-12-17 17:57:35.531][000000003.629] D/mobile LUAT_MOBILE_CC_READY
[2025-12-17 17:57:36.383][000000004.483] I/user.TTS 播放结束: 我是任务 B
[2025-12-17 17:57:36.385][000000004.484] I/user.LOCK 锁已释放
[2025-12-17 17:57:37.394][000000005.482] I/user.TTS 准备播放: 我是任务 A
[2025-12-17 17:57:37.397][000000005.483] I/user.LOCK 我是任务 A 拿到锁了,开始播放...
[2025-12-17 17:57:37.398][000000005.484] I/user.TTS 准备播放: 我是任务 B
[2025-12-17 17:57:39.385][000000007.483] I/user.TTS 播放结束: 我是任务 A
[2025-12-17 17:57:39.388][000000007.484] I/user.LOCK 锁已释放
[2025-12-17 17:57:39.391][000000007.485] I/user.LOCK 我是任务 B 拿到锁了,开始播放...
[2025-12-17 17:57:41.390][000000009.485] I/user.TTS 播放结束: 我是任务 B
[2025-12-17 17:57:41.394][000000009.486] I/user.LOCK 锁已释放
[2025-12-17 17:57:42.389][000000010.484] I/user.TTS 准备播放: 我是任务 A
[2025-12-17 17:57:42.393][000000010.485] I/user.LOCK 我是任务 A 拿到锁了,开始播放...
[2025-12-17 17:57:42.395][000000010.486] I/user.TTS 准备播放: 我是任务 B
[2025-12-17 17:57:44.384][000000012.485] I/user.TTS 播放结束: 我是任务 A
[2025-12-17 17:57:44.387][000000012.486] I/user.LOCK 锁已释放
[2025-12-17 17:57:44.390][000000012.487] I/user.LOCK 我是任务 B 拿到锁了,开始播放...
[2025-12-17 17:57:46.387][000000014.487] I/user.TTS 播放结束: 我是任务 B
[2025-12-17 17:57:46.390][000000014.488] I/user.LOCK 锁已释放
[2025-12-17 17:57:47.384][000000015.486] I/user.TTS 准备播放: 我是任务 A
[2025-12-17 17:57:47.387][000000015.487] I/user.LOCK 我是任务 A 拿到锁了,开始播放...
[2025-12-17 17:57:47.389][000000015.488] I/user.TTS 准备播放: 我是任务 B
[2025-12-17 17:57:49.400][000000017.487] I/user.TTS 播放结束: 我是任务 A
[2025-12-17 17:57:49.403][000000017.488] I/user.LOCK 锁已释放
[2025-12-17 17:57:49.404][000000017.489] I/user.LOCK 我是任务 B 拿到锁了,开始播放...
[2025-12-17 17:57:51.402][000000019.489] I/user.TTS 播放结束: 我是任务 B
[2025-12-17 17:57:51.405][000000019.490] I/user.LOCK 锁已释放
[2025-12-17 17:57:52.399][000000020.488] I/user.TTS 准备播放: 我是任务 A
[2025-12-17 17:57:52.405][000000020.489] I/user.LOCK 我是任务 A 拿到锁了,开始播放...
[2025-12-17 17:57:52.407][000000020.490] I/user.TTS 准备播放: 我是任务 B
[2025-12-17 17:57:54.390][000000022.489] I/user.TTS 播放结束: 我是任务 A
[2025-12-17 17:57:54.393][000000022.490] I/user.LOCK 锁已释放
[2025-12-17 17:57:54.396][000000022.491] I/user.LOCK 我是任务 B 拿到锁了,开始播放...
[2025-12-17 17:57:56.390][000000024.491] I/user.TTS 播放结束: 我是任务 B
[2025-12-17 17:57:56.394][000000024.492] I/user.LOCK 锁已释放

分析每个任务持有的时长,可以看到锁是成功起效了,并没有出现ab同时抢任务的问题


以上就是我的设计,虽然大家一般用lua都不会开发很复杂的程序,但是凡是都有个例外,此外,还有人可能会说,那我用table实现队列,不也能执行你这个操作吗,你这个操作的好处是什么?用锁的话主要是更线性更符合直觉,队列需要关注时序的问题,还需要一个队列常驻后台实现轮询/等待唤起这样。

相关推荐
每天回答3个问题8 天前
UE教程|unlua知识地图
ue5·腾讯·lua5.4
jumu2028 天前
无人船、AUV与无人车编队路径跟踪的奇妙探索
lua5.4
初见无风2 个月前
3.1 Lua代码中的元表与元方法
开发语言·lua·lua5.4
初见无风2 个月前
3.3 Lua代码中的协程
开发语言·lua·lua5.4
初见无风2 个月前
3.0 Lua代码中的闭包
开发语言·lua·lua5.4
初见无风2 个月前
2.5 Lua代码中string类型常用API
开发语言·lua·lua5.4
初见无风2 个月前
2.7 Lua代码中的可变参数
开发语言·lua·lua5.4
初见无风2 个月前
2.4 Lua代码中table常用API
开发语言·lua·lua5.4
初见无风2 个月前
2.6 Lua代码中function的常见用法
开发语言·lua·lua5.4