Skip to content

coroutine — 协程 ​

模块名:coroutine

标准 Lua 协程库(Lua 5.4)。协程可以将「分多步、中间需要等待」的逻辑写成一条顺序执行的函数,用 yield 主动让出、resume 恢复,而不必拆成多个回调。

在 LPPLC 上协程尤其有用:配合 system.sleep,可以在协程里编写「等 1 秒再执行下一步」这类代码而不阻塞事件循环(system.sleep 在协程里是注册定时器后 yield,到时自动恢复)。协程采用协作式调度,并非真正并行,同一时刻只有一个协程在执行。

协程里的 sleep 为什么不卡住整个程序:主线程与协程的 resume/yield 时序

要跑多个时,改用 task ​

本页讲的是裸 coroutine 的用法。如果你要同时跑好几段这样的流程, 用 task 更省事 —— 它内部就是协程,但替你管了下面几件事:

自己用 coroutine用 task
要自己 coroutine.wrap 创建task.start(function() ... end) 一行
要自己用 timer.start 起头,否则创建了也不会跑起了就跑
多个协程要自己排调度调度器统一推进
想停要自己维护引用task.stop(tid)
想等一个事件要自己搭订阅event.wait
—上限 32 个,超出会报错

只偶尔用一次,本页的用法就够;要长期跑、要多个、要等事件,用 task。 两者可以同时用,互不冲突。

函数总览 ​

函数作用
coroutine.create用一个函数创建协程,返回协程对象(不启动)
coroutine.resume(...)启动或恢复协程运行
coroutine.yield(...)在协程内让出,挂起并把值返回给 resume 方
coroutine.status查询协程状态
coroutine.wrap创建协程并返回一个恢复它的函数
coroutine.isyieldable当前是否处于可 yield 的上下文
coroutine.running返回当前协程及它是否为主线程
coroutine.close关闭一个挂起或出错的协程,回收资源

coroutine.create ​

lua
coroutine.create(f) --> thread

用函数 f 创建一个协程,只创建不运行。

参数

#参数类型必填默认值说明
1ffunction是—协程主体函数

返回值

类型说明
thread协程对象,交给 coroutine.resume 运行

示例

lua
local co = coroutine.create(function(a, b)
    print("start", a, b)
    coroutine.yield()
    print("resumed")
end)
coroutine.resume(co, 1, 2)   -- 打印 start 1 2
coroutine.resume(co)         -- 打印 resumed

coroutine.resume ​

lua
coroutine.resume(co, [...]) --> boolean, ...

启动或恢复协程。首次 resume 时,额外参数传给协程主体函数;后续 resume 时,额外参数作为 yield 的返回值。

参数

#参数类型必填默认值说明
1cothread是—协程对象
2…...any否—首次是主体函数的入参;之后是 yield 的返回值

返回值

类型说明
boolean是否成功。协程内报错时为 false
...成功时是 yield/return 带出的值;失败时第 2 个返回值是错误信息

说明

  • resume 不会把协程里的错误往外抛,而是以 false, errmsg 的形式返回。要感知错误必须检查第一个返回值。

实用例程 ​

lua
co2=coroutine.create(function()--建立协程2
	--数据上传
	x=mqtt.creat("192.168.1.100", 1883, "<名称>", "<名称>", "<名称>")--建立 MQTT 对象
	x:start()--启动 MQTT 协议栈

	dssc=timer.start(60000,
	  function()--启动定时器 60 秒发布一个数据

		if mqtt.is_linked(x) and  plc.d(5)==0 then
		  local sw = plc.d(4) /1000
		  local jsonstring =[[{"id":123,"dp":{"sw":[{"v":]]..sw..[[}],"err_num":[{"v":]]..plc.d(6)..[[}]}}]]
		  print(jsonstring)
		  mqtt.publish(x,"$sys/<设备ID或凭据>/xfsc_sw/dp/post/json",jsonstring,0)--支持 QOS0,QOS1,QOS2
		end
	  end
	)		
  end
)
coroutine.resume(co2)--启动协程(上面只创建了 co2;每个 create 出来的协程都要自己 resume 才会跑)

把上面的地址、主题、引脚替换成你项目里的即可。

coroutine.yield ​

lua
coroutine.yield([...]) --> ...

在协程内部让出执行权,挂起协程,把参数作为 resume 的返回值传出去。下次 resume 时从这里继续,yield 的返回值就是那次 resume 传入的额外参数。

参数

#参数类型必填默认值说明
1…...any否—传回给 resume 方的值

返回值

类型说明
...下次 resume 时传入的额外参数

说明

  • 只能在协程内部调用。在主线程直接调用会报错(见 coroutine.isyieldable)。

coroutine.status ​

lua
coroutine.status(co) --> string

查询协程当前状态。

参数

#参数类型必填默认值说明
1cothread是—协程对象

返回值

类型说明
string"suspended" 挂起(可恢复)| "running" 正在运行 | "normal" 恢复了别的协程后自身处于活动但非运行 | "dead" 已结束或出错

coroutine.wrap ​

lua
coroutine.wrap(f) --> function

创建协程并返回一个函数;每次调用该函数就相当于 resume 一次,但直接返回 yield 的值(不带成功标志),协程内出错时直接把错误抛出。

参数

#参数类型必填默认值说明
1ffunction是—协程主体函数

返回值

类型说明
function调用它来驱动协程,返回值即 yield/return 带出的值

示例

lua
local gen = coroutine.wrap(function()
    for i = 1, 3 do coroutine.yield(i) end
end)
print(gen(), gen(), gen())   -- 1  2  3

说明

  • 写法比 create+resume 简洁,适合做迭代器;缺点是协程内出错会直接抛,需要外层 pcall。

coroutine.isyieldable ​

lua
coroutine.isyieldable() --> boolean

判断当前上下文是否可以 yield(即是否在协程里)。

参数

无参数。

返回值

类型说明
boolean在协程内为 true,在主线程为 false

coroutine.running ​

lua
coroutine.running() --> thread, boolean

返回当前正在运行的协程,以及它是否为主线程。

参数

无参数。

返回值

类型说明
thread当前协程对象
boolean是否为主线程(在主线程调用时为 true)

coroutine.close ​

lua
coroutine.close(co) --> boolean [, string]

关闭一个处于 suspended 或 dead 状态的协程,运行其中挂起的 to-be-closed 变量并回收资源,之后该协程变为 dead。

参数

#参数类型必填默认值说明
1cothread是—要关闭的协程

返回值

类型说明
boolean是否成功
string失败时的错误信息

说明

  • 不能关闭正在运行(running/normal)的协程。

例程 ​

每个例程都是可直接下载运行的完整脚本:注册完定时器后脚本就结束,固件自动进入消息泵。 API 只用本页已文档化的函数;统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。

例程 1:把「工序」写成一段顺序代码(协程 + system.sleep) ​

适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / 软PLC模拟器 | 依赖模块:coroutine system timer plc | 难度:★☆☆

场景:灌装机的一轮动作是"开阀 → 等 2 秒 → 关阀 → 等 1 秒 → 通知完成"。用回调写要拆成三四层嵌套定时器;用协程写就是一段从上到下的顺序代码。关键在于里面的 system.sleep 只在协程里才不阻塞:它注册定时器后 yield,到点由主循环恢复。

完整代码(可直接下载运行):

lua
-- 一轮工序写成顺序代码;等待由协程让出,不卡主循环
local busy = false                       -- 防止上一轮没跑完又排一轮

local function one_cycle()
    busy = true
    plc.y(0, 1)                          -- 开阀
    print("开阀")
    system.sleep(2000)                   -- 协程内 yield,期间回调照常

    plc.y(0, 0)                          -- 关阀
    print("关阀")
    system.sleep(1000)

    plc.m(50, 1)                         -- 通知梯形图:一轮完成
    print("一轮动作完成")
    busy = false
end

timer.start(1000, function()
    if busy or plc.x(0) ~= 1 then return end
    plc.m(50, 0)
    coroutine.wrap(one_cycle)()          -- 每次启动一条新协程跑一轮
end)

要点:

  • system.sleep 在协程内才是非阻塞的(注册定时器 → yield → 到时 resume);主线程里同一个调用会冻结整台设备。
  • coroutine.wrap(f) 返回的是一个"调用即 resume"的函数,比 create + resume 简洁;代价是协程内报错会直接抛出,外层要 pcall 兜。
  • busy 标志必须有:一轮没跑完又排一轮,两条协程会同时动同一个 Y,动作就乱了。
  • 协程是协作式的:得靠 yield / sleep 主动让出才轮到别人;纯 CPU 的死循环照样卡住事件循环。
  • 与梯形图的接口用 M / D(这里是 M50),不要用 Lua 全局变量跨任务同步(见并发模型概述)。

验证方法:

  1. 让 X0 保持 ON:日志按 开阀 →(2 秒)→ 关阀 →(1 秒)→ 一轮动作完成 的顺序出现,Y0 随之通断。
  2. 这 3 秒等待期间,另一个 timer.start(500, …) 或 MQTT 下发仍然正常响应(证明没有阻塞主循环)。
  3. 一轮没结束时再次满足触发条件:因为 busy 为 true,不会叠加第二轮(日志不会串味)。
  4. 把这段搬到主线程直接调用(去掉 coroutine.wrap)作为对照:设备会卡住约 3 秒、期间所有回调失效。

注意事项:

  • 忘了 coroutine.wrap 直接调用 one_cycle():那就在主线程跑了,sleep 变成真阻塞。
  • coroutine.wrap 会抛出协程内的错误;用 coroutine.create + resume 时错误藏在返回值的第二个里(false, err),不检查就"什么都不发生"。
  • 协程里不要去动别人正在用的输出点,协程之间没有任何互斥机制。

例程 2:轮询多台从站,每台之间让出(不一次做完) ​

适用:同上 | 依赖模块:coroutine system timer plc | 难度:★★☆

场景:一条 RS485 上挂 20 台从站,逐台查询要花好几秒。串行做完会把整台设备憋死;拆成"查一台让出 200ms",总耗时没变,但期间依然收云平台下发、依然(pump)其它回调。这里用协程保留"看起来顺序"的写法。

完整代码(可直接下载运行):

lua
-- 轮询 1~20 号从站:查一台让出 200ms,整条链路上事件循环全程不卡
local SLAVES = 20

local function read_slave(addr)
    -- 现场把这里换成真正的取数逻辑(com_modbus 的 data 对象读回、
    -- 或 plc.get 从远端搬回来的值);示意用:读 D(1000+addr)
    return plc.d(1000 + addr)
end

local function poll_all()
    for addr = 1, SLAVES do
        local ok, val = pcall(read_slave, addr)
        if ok then
            plc.d(2000 + addr, val)               -- 结果落到 D2001 起
        else
            print("从站读取失败:", addr, val)
        end
        system.sleep(200)                          -- 协程内:让出一拍
    end
    plc.m(51, 1)                                   -- 一轮巡检完成
    print("巡检完成,共", SLAVES, "台")
end

timer.start(60000, function()                      -- 每分钟巡检一轮
    plc.m(51, 0)
    coroutine.wrap(poll_all)()
end)

要点:

  • pcall(read_slave, addr) 把"某一台掉线"关在这一台里,不会让整轮巡检中断。
  • 每台之间 system.sleep(200):既给总线留出应答时间,也给主循环喘息。这就是"长耗时任务"在本设备上的标准处理方式。
  • 结果统一落到 D 区(这里从 D2001 起),梯形图和云端都从 D 区取,不读 Lua 变量。
  • 完成标志用 M51,梯形图/画面据此刷新"最后更新时间"。

验证方法:

  1. 下载运行:日志出现 巡检完成,共 20 台,M51 置 ON;D2001 起每 60 秒更新一轮。
  2. 轮询进行中(约 4 秒内)向设备发一条 MQTT 下发或看另一个定时器的日志:仍然在跑
  3. 故意让某一台读失败(注释掉 plc.d 写入改成一个会报错的调用):日志只出现一条失败,后面 addr 继续 —— pcall 生效。
  4. 把 system.sleep(200) 删掉作为对照:整轮瞬间跑完,但期间分发被占满,其它回调明显滞后。

注意事项:

  • 协程解决的是"等待时不卡",不是加速:总耗时一样。真要更快得靠 system.start_thread(仅固件)。
  • 一次 timer.start 排一轮,别在被触发的同一个 tick 里排多轮。
  • 每台 sleep 太短(比如 10ms)意义不大 —— 定时器最小周期是 10ms,太密会挤占主线程处理别的消息。

例程 3:用 create + resume 掌握每一步的返回值(外部推着走) ​

适用:同上 | 依赖模块:coroutine timer plc | 难度:★★☆

场景:有些流程需要协程告诉外面自己走到哪一步(比如"当前在第 3 步"),而且要由外部决定何时往下走 —— 这时用 coroutine.create + coroutine.resume,从返回值里拿进度,还能检查 false, err 拿到协程里的错误(wrap 则会直接抛)。

完整代码(可直接下载运行):

lua
-- 用 create + resume 拿到协程的每一步返回值,并检查协程是否出错
local steps = coroutine.create(function(total)
    for i = 1, total do
        plc.d(400, i)                    -- 把当前步号写进 D400
        coroutine.yield("step " .. i)    -- 交出进度字符串
    end
    return "done"
end)

timer.start(1000, function()
    if coroutine.status(steps) == "dead" then return end   -- 已经跑完就不再 resume

    local ok, ret = coroutine.resume(steps, 5)
    if not ok then
        print("协程出错:", ret)          -- resume 不抛错,错误在第 2 个返回值里
        return
    end
    print("推进到:", ret, "状态:", coroutine.status(steps))
end)

要点:

  • coroutine.resume 第一个返回值是"有没有错":必须检查,否则协程里出错表现为"悄悄停了"。
  • yield 带出的值由 resume 的返回值带回;反过来,resume 后续传入的参数会成为 yield 的返回值。
  • 用 coroutine.status(co) 判断还能不能 resume("dead" 说明已结束或出错,再 resume 只返回 false)。
  • coroutine.close(co) 可以关掉一个挂起的协程回收资源;正在运行(running/normal)的不能关。
  • 这种模式适合"每拍推进一步",由外部主动推进比 wrap 更可控。

验证方法:

  1. 下载运行后每秒打印 推进到: step N 状态: suspended,直到出现 done,之后不再推进(status 已 dead)。
  2. 上位机监视 D400:每步 +1,最后停在 5。
  3. 把协程里的 coroutine.yield(...) 改成调用一个不存在的函数:日志打印 协程出错: …(resume 的第二个返回值),不会抛到主循环。
  4. 去掉 status == "dead" 判断作为对照:跑完之后每拍打印 false, cannot resume dead coroutine 之类信息,逻辑上更容易出错。

注意事项:

  • coroutine.wrap 与 create 的错误处理完全相反:wrap 抛错、create+resume 返回 false, err,混着写会漏错误。
  • 协程不是线程:永远优先想到"它什么时候 yield",而不是"它在后台跑"。
  • 主线程里直接 coroutine.yield() 会报错;判断是否可 yield 用 coroutine.isyieldable()。

例程 4:现场常用脚本 协程.LPLUA(六个接口一次跑完,原文) ​

适用:同上 | 依赖模块:coroutine | 难度:★☆☆

场景:想一次看清 create / resume / status / wrap / yield / running 到底各打印什么。 这份脚本把六个接口全部走一遍(文件 协程.LPLUA),原文收录如下。

完整代码(协程.LPLUA 原文):

lua
-- coroutine_test.lua 文件
co = coroutine.create(
    function(i)
        print(i);
    end
)


coroutine.resume(co, 1)   -- 1
print(coroutine.status(co))  -- dead
 
print("----------")
 
co = coroutine.wrap(
    function(i)
        print(i);
    end
)
 
co(1)
 
print("----------")
 
co2 = coroutine.create(
    function()
        for i=1,10 do
            print(i)
            if i == 3 then
                print(coroutine.status(co2))  --running
                print(coroutine.running()) --thread:XXXXXX
            end
            coroutine.yield()
        end
    end
)
 


coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
 
print(coroutine.status(co2))   -- suspended
print(coroutine.running())
 
print("----------")

要点:

  • 三段分别对应三种用法:create + resume(跑完就 dead)、wrap(返回的表当函数直接调)、create + 多次 resume(每调一次走一段 yield)。
  • coroutine.status(co2) 在外面打印是 suspended(挂在 yield 上),在协程自己内部打印自己才是 running —— 原脚本 i == 3 那两句就是专门演示这个差的。
  • coroutine.running() 返回两个值:当前线程对象 + 一个布尔值(是不是主线程)。协程里那句会多印一个 true,脚本末尾那句在主线程调、印 false;原脚本注释只写了 thread:XXXXXX,看到多出来的 true/false 不是出错。
  • 末尾 co2 停在 suspended 不再有人 resume,这份脚本跑完就结束 —— 它是语法示范,不是常驻任务(常驻写法见例程 5)。

验证方法:

  1. 日志依次应看到:1 → dead → 分隔线 → 1 → 分隔线 → 1 2 3 running thread: … true → suspended thread: … false → 分隔线。
  2. 把三个 coroutine.resume(co2) 减到两个:最后一行 status 应变成 suspended 但只打印到 2(确认“一次 resume 只推进一段”)。

注意事项:

  • 原脚本里的注释(-- 1、-- dead)标的是那一行自己产出的内容,不是它旁边的 print 的返回值;对照日志看容易错位。
  • wrap 出来的函数没有“有没有错”这个返回值,协程里一报错直接抛到调用处(与 create + resume 完全相反)。

例程 5:现场常用脚本 协程1.LPLUA(两个常驻任务并行,原文) ​

适用:同上 | 依赖模块:coroutine system | 难度:★★☆

场景:想让两件互不相干的活各自每秒跑一次、又不想被定时器回调把它们串在一起(文件 协程1.LPLUA),原文收录如下。

完整代码(协程1.LPLUA 原文):

lua
co1=coroutine.create(function()--建立协程1
	while true do --一直循环执行
		print("co1 run")--打印输出
		system.sleep(1000)--延时1秒
	end
end
)
co2=coroutine.create(function()--建立协程2
	while true do --一直循环执行
		print("co2 run")--打印输出
		system.sleep(1000)--延时1秒
	end
end
)
coroutine.resume(co2)--启动协程(上面只创建了 co2;每个 create 出来的协程都要自己 resume 才会跑)

要点:

  • 这才是现场真正用来跑两个常驻任务的写法:while true + system.sleep(1000)。
  • 关键在 system.sleep 在协程里不是睡觉,而是让出:固件先把当前协程登记一个引用防止被回收,挂上一次性定时器,然后 lua_yield 把协程交出去;定时到点后往 Lua 消息队列投一条唤醒消息,主循环取出来再恢复这个协程。所以两行 coroutine.resume 都能立刻返回、co1 与 co2 都启动成功,之后每秒各打印一次,互不抢占也不卡主线程。
  • ⚠ 这条链依赖主循环在泵消息队列:脚本其他地方只要有主线程里的 system.sleep 或不带 yield 的死循环,两个协程就会停在半路不动,看着像“任务突然不跑了”。
  • ⚠ 反过来:在 system.start_thread 起的独立线程里,system.sleep 走的是真阻塞(不让出),那种上下文里这份脚本的 co2 永远起不来 —— 要并行请用协程配这个写法,而不是在独立线程里套协程。
  • co1 / co2 赋值给全局正好挡住了“协程被 GC”这条路(让出期间固件也会临时护住,但醒来后仍需您自己持有)。

验证方法:

  1. 下载运行后,日志每秒出现 co1 run 与 co2 run 各一行,且两者时间戳差很小 —— 这就是“两个任务并行且没卡主线程”。
  2. 另加一个 timer.start(500, function() print("alive") end):alive 应一直按 500ms 出现,证明协程没有占住主循环。
  3. 对照实验(只在模拟器或试验设备上做):把 co1 里的 system.sleep(1000) 那一行注掉 —— 日志刷不出 co2 run、定时器也不动了,因为 co1 再也不会让出。这能直观看到“协作式”三个字的意思。

注意事项:

  • while true 没有退出条件,这两个协程会一直活着;重新下载脚本才会被清掉。需要停时在循环里加条件让协程自己 return(而不是从外面“杀”它)。
  • 只 create 不 resume:什么都不会发生,且不会报错 —— 这是这份脚本最后两行不能省的原因。
  • 两个协程读写同一个 D/M 寄存器时仍会互相踩:协程不是隔离,只是轮流执行,共用数据照样要自己排序。

注意事项 ​

  • 协程是协作式的:只有当前协程主动 yield 或结束,别的代码才有机会运行。协程里做纯 CPU 的死循环同样会卡住事件循环。
  • coroutine.resume 吞掉错误(返回 false, err),调试时记得检查返回值;coroutine.wrap 则会把错误抛出,需要外层 pcall。
  • 在协程里调用 system.sleep 才是非阻塞的;在主线程里调用是真阻塞,区别见 并发模型概述。
  • 协程不会提升运算能力,它解决的是「代码写成顺序流程」和「等待时不卡主循环」。真正耗时很久的活要拆成小步,做法见 并发模型概述 的「长耗时任务怎么处理」。

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研