经常使用freertos进行开发的用户都会经常使用互斥锁的功能,主要是用于实现原子操作,避免冲突,但是在合宙的luatos上,其实没有相关的api接口,lua本身也不支持这样操作,于是本人研究了一下,写出了一个库用于实现互斥锁的功能,可能不太完善,欢迎交流
实现原理
有人会说,lua本身就是协程式的多仍任务,要锁做什么,是这样的,确实也不需要,但是复杂任务的时候,比如串口抢占,websocket抢占,都涉及到先后操作和逻辑竞争,这个时候就需要锁了,而要实现互斥锁,我们需要两个核心的api sys.waitUntil和sys.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实现队列,不也能执行你这个操作吗,你这个操作的好处是什么?用锁的话主要是更线性更符合直觉,队列需要关注时序的问题,还需要一个队列常驻后台实现轮询/等待唤起这样。