协同程序 (Coroutines) Lua 协同程序 (Coroutines) 详解与实践 协同程序 (Coroutines) 是 Lua 语言中一种强大的特性,它提供了一种实现协作式多任务的方式。与线程 (Threads) 的抢占式多任务不同,协同程序允许你在代码的特定点主动挂起执行,并将控制权交还给调用者,稍后又能从挂起的地方恢复执行。这种机制在处理异步操作、迭代器、状态机以及需要并发但又不想引入线程复杂性的场景中非常有用。 什么是协同程序 (Coroutines)? 简单来说,协同程序可以被视为轻量级的线程,但它们运行在同一个线程中。 关键的区别在于: 协作式: 协同程序的主动权在于自身。它必须显式地 (让出) 执行权,才能被挂起。
协同程序 (Coroutines) 是 Lua 语言中一种强大的特性,它提供了一种实现协作式多任务的方式。与线程 (Threads) 的抢占式多任务不同,协同程序允许你在代码的特定点主动挂起执行,并将控制权交还给调用者,稍后又能从挂起的地方恢复执行。这种机制在处理异步操作、迭代器、状态机以及需要并发但又不想引入线程复杂性的场景中非常有用。
简单来说,协同程序可以被视为轻量级的线程,但它们运行在同一个线程中。 关键的区别在于:
协作式: 协同程序的主动权在于自身。它必须显式地 yield (让出) 执行权,才能被挂起。
单线程: 所有协同程序都运行在同一个 Lua 线程中,这意味着它们之间不会发生真正的并行执行,而是时间片轮转式的交替执行。
非抢占式: Lua 虚拟机不会在任意时刻打断协同程序的执行。只有当协同程序调用 yield 时,控制权才会转移。
类比: 你可以将协同程序想象成舞台剧中的演员。
主程序 (Main Program): 舞台导演,负责指挥和调度。
协同程序 (Coroutines): 演员,各自负责表演自己的剧本片段。
yield: 演员表演完一段剧情后,暂时退场 (挂起),等待导演的指令 (resume) 再次登场。
resume: 导演指示演员重新登场,继续之前的表演 (恢复执行)。
为什么使用协同程序?
简化异步编程: 避免回调地狱,使异步代码更易读和维护。
实现迭代器: 创建自定义的迭代器,用于遍历复杂的数据结构或生成序列。
状态机: 构建复杂的状态机逻辑,每个状态可以作为一个协同程序。
并发模型: 在单线程环境下模拟并发,提高程序的响应性,尤其在 I/O 密集型任务中。
游戏开发: 处理动画、AI、事件序列等,避免阻塞主循环。
Lua 提供了 coroutine 库来管理协同程序。以下是核心 API 函数:
coroutine.create (f)功能: 创建一个新的协同程序。
参数: f - 一个函数,表示协同程序要执行的代码。
返回值: 返回一个 thread 类型的值,代表新创建的协同程序。
重要: coroutine.create 不会启动 协同程序,它只是创建了一个协同程序对象,等待被激活。
代码示例:
local co = coroutine.create(function() print("协同程序开始执行") coroutine.yield() -- 首次挂起 print("协同程序恢复执行") end) print("主程序继续执行")
这段代码创建了一个协同程序 co,但此时协同程序的代码 尚未执行。 运行这段代码只会输出 "主程序继续执行"。
coroutine.resume (co, ...)功能: 启动或恢复一个协同程序的执行。
参数:
co - 要恢复的协同程序 (由 coroutine.create 返回的 thread 对象)。
... - 传递给协同程序的参数 (首次启动时) 或传递给 yield 的返回值 (恢复执行时)。
返回值:
第一个返回值是状态:true (成功恢复并执行到 yield 或结束) 或 false (发生错误)。
如果成功,后续返回值是协同程序 yield 传递的值 (如果协同程序 yield 了) 或协同程序函数返回的值 (如果协同程序执行结束)。
如果失败,第二个返回值是错误信息。
代码示例 (继续上面的例子):
local co = coroutine.create(function() print("协同程序开始执行") local value = coroutine.yield("首次 yield") -- 首次挂起并传递 "首次 yield" print("协同程序恢复执行,接收到的值:", value) return "协同程序结束" end) print("主程序开始恢复协同程序") local status, result = coroutine.resume(co) -- 首次恢复 print("首次恢复状态:", status, "结果:", result) print("主程序再次恢复协同程序") status, result = coroutine.resume(co, "来自主程序的参数") -- 再次恢复并传递参数 print("再次恢复状态:", status, "结果:", result) print("主程序执行结束")
输出结果:
主程序继续执行 主程序开始恢复协同程序 协同程序开始执行 首次恢复状态: true 结果: 首次 yield 主程序再次恢复协同程序 协同程序恢复执行,接收到的值: 来自主程序的参数 再次恢复状态: true 结果: 协同程序结束 主程序执行结束
代码解释:
coroutine.resume(co) 首次恢复协同程序 co 的执行。协同程序开始执行,输出 "协同程序开始执行",然后遇到 coroutine.yield("首次 yield"),协同程序挂起,并将 "首次 yield" 返回给 coroutine.resume。
coroutine.resume(co) 的第一个返回值 status 为 true (成功),第二个返回值 result 为 "首次 yield"。
coroutine.resume(co, "来自主程序的参数") 再次恢复协同程序 co 的执行,并将 "来自主程序的参数" 作为参数传递给 yield 表达式。协同程序从 yield 处恢复执行,并将接收到的参数赋值给 value,输出 "协同程序恢复执行,接收到的值: 来自主程序的参数",然后执行到 return "协同程序结束",协同程序结束执行,并将 "协同程序结束" 返回给 coroutine.resume。
第二次 coroutine.resume(co) 的第一个返回值 status 为 true (成功),第二个返回值 result 为 "协同程序结束"。
coroutine.yield (...)功能: 挂起当前协同程序的执行,并将控制权交还给调用者 (通常是调用 resume 的主程序或其他协同程序)。
参数: ... - 可选的参数,这些参数会作为 coroutine.resume 的返回值返回给调用者。
返回值: 当协同程序被 coroutine.resume 恢复执行时,coroutine.yield 表达式会接收到 coroutine.resume 传递的参数。
重要: coroutine.yield 只能在协同程序的函数体内直接调用,不能在嵌套函数或匿名函数中调用。
代码示例 (迭代器):
function number_generator(n) return coroutine.create(function() for i = 1, n do coroutine.yield(i) -- 每次 yield 一个数字 end end) end local generator = number_generator(5) while true do local status, num = coroutine.resume(generator) if not status then -- 协同程序执行结束或出错 break end print("生成的数字:", num) end
输出结果:
生成的数字: 1 生成的数字: 2 生成的数字: 3 生成的数字: 4 生成的数字: 5
代码解释:
number_generator(n) 函数创建并返回一个协同程序,该协同程序负责生成从 1 到 n 的数字。
while true 循环不断恢复 generator 协同程序的执行。
每次 coroutine.yield(i) 挂起协同程序,并将数字 i 返回给 coroutine.resume。
当循环结束 (i > n),协同程序函数执行完毕,coroutine.resume 返回 false (表示协同程序结束),循环退出。
coroutine.status (co)功能: 返回指定协同程序的状态。
参数: co - 要查询状态的协同程序。
返回值: 一个字符串,表示协同程序的状态:
"running": 协同程序正在运行 (只有在协同程序内部调用 coroutine.status 才能看到这个状态)。
"suspended": 协同程序已挂起 (调用了 yield 或刚创建但未启动)。
"normal": 协同程序是活动的,但不是运行状态 (例如,它正在等待被 resume,或者它调用了另一个协同程序)。
"dead": 协同程序已执行完毕或发生错误而终止。
代码示例:
local co = coroutine.create(function() print("协同程序开始") coroutine.yield() print("协同程序恢复") end) print("协同程序状态:", coroutine.status(co)) -- 输出 "suspended" coroutine.resume(co) print("协同程序状态:", coroutine.status(co)) -- 输出 "suspended" (再次 yield) coroutine.resume(co) print("协同程序状态:", coroutine.status(co)) -- 输出 "dead" (执行结束)
coroutine.wrap (f)功能: 创建一个 wrapper 函数,每次调用这个 wrapper 函数都会 resume 对应的协同程序。
参数: f - 一个函数,表示协同程序要执行的代码。
返回值: 返回一个函数 (wrapper 函数)。
重要: 与 coroutine.create 不同,coroutine.wrap 返回的是一个函数,而不是 thread 对象。 每次调用这个返回的函数,都会 resume 协同程序,直到协同程序 yield 或结束。 如果协同程序发生错误,错误会传播到 wrapper 函数的调用者。
代码示例 (简化迭代器):
function number_generator_wrapped(n) return coroutine.wrap(function() for i = 1, n do coroutine.yield(i) end end) end local generator_wrap = number_generator_wrapped(3) while true do local num = generator_wrap() -- 调用 wrapper 函数 resume 协同程序 if num == nil then -- 协同程序执行结束,wrapper 函数返回 nil break end print("生成的数字:", num) end
输出结果:
生成的数字: 1 生成的数字: 2 生成的数字: 3
代码解释:
coroutine.wrap 创建了一个 wrapper 函数 generator_wrap,每次调用 generator_wrap() 都会 resume 内部的协同程序。
当协同程序 yield 时,generator_wrap() 返回 yield 的值。
当协同程序执行结束时,generator_wrap() 返回 nil。
使用 while true 循环和 generator_wrap() 返回值是否为 nil 来判断迭代是否结束,代码更加简洁。
coroutine.running ()功能: 返回当前正在运行的协同程序 ( thread 对象)。
参数: 无
返回值:
如果在协同程序内部调用,返回当前协同程序的 thread 对象。
如果在主程序环境中调用,返回 nil。
用途: 协同程序可以获取自身的引用,例如用于自我管理或传递给其他协同程序。
代码示例:
local co1 co1 = coroutine.create(function() print("协同程序 co1 正在运行,自身引用:", coroutine.running()) coroutine.yield() print("协同程序 co1 恢复运行") end) local co2 = coroutine.create(function() print("协同程序 co2 正在运行,自身引用:", coroutine.running()) coroutine.resume(co1) -- 在 co2 中 resume co1 print("协同程序 co2 继续运行") end) print("主程序环境 running:", coroutine.running()) -- 输出 nil coroutine.resume(co2) coroutine.resume(co2) -- 再次 resume co2,co2 会继续执行直到结束
输出结果:
主程序环境 running: nil 协同程序 co2 正在运行,自身引用: thread: 0x... (co2 的地址) 协同程序 co1 正在运行,自身引用: thread: 0x... (co1 的地址) 协同程序 co1 恢复运行 协同程序 co2 继续运行
协同程序非常适合实现协作式多任务,模拟并发执行多个任务,但不需要线程的复杂性。每个任务可以作为一个协同程序,通过 yield 让出 CPU 时间,让其他任务有机会执行。
代码示例 (简单的任务调度器):
local tasks = {} function create_task(name, func) local co = coroutine.create(function() print("任务", name, "开始执行") func() print("任务", name, "执行结束") end) table.insert(tasks, co) end function task1_func() for i = 1, 3 do print("任务 1: 执行步骤", i) coroutine.yield() -- 让出时间 end end function task2_func() for i = 1, 2 do print("任务 2: 执行步骤", i) coroutine.yield() -- 让出时间 end end create_task("任务 1", task1_func) create_task("任务 2", task2_func) print("开始调度任务") while #tasks > 0 do local task = table.remove(tasks, 1) -- 从队列中取出一个任务 local status = coroutine.resume(task) if coroutine.status(task) ~= "dead" then -- 如果任务未完成,放回队列 table.insert(tasks, task) end end print("任务调度结束")
输出结果 (任务交替执行):
开始调度任务 任务 任务 1 开始执行 任务 1: 执行步骤 1 任务 任务 2 开始执行 任务 2: 执行步骤 1 任务 1: 执行步骤 2 任务 2: 执行步骤 2 任务 1: 执行步骤 3 任务 任务 1 执行结束 任务 任务 2 执行结束 任务调度结束
代码解释:
create_task 函数创建任务协同程序并添加到 tasks 队列。
调度器 while 循环不断从 tasks 队列中取出任务并 resume 执行。
每个任务在执行一段时间后通过 coroutine.yield() 让出 CPU 时间。
如果任务尚未完成 (状态不是 "dead"),则重新加入队列,等待下次调度。
任务 1 和 任务 2 交替执行,模拟了并发效果。
协同程序非常适合创建自定义的迭代器,用于遍历复杂的数据结构或生成序列。 coroutine.yield 允许在迭代过程中暂停和恢复,使得迭代逻辑更清晰易懂。
代码示例 (树形结构迭代器 - 前序遍历):
local Tree = { value = nil, left = nil, right = nil } function Tree:new(value, left, right) local tree = {value = value, left = left, right = right} setmetatable(tree, self) self.__index = self return tree end function Tree:preorder_iterator() return coroutine.create(function(node) local stack = {node} while #stack > 0 do local current_node = table.remove(stack) if current_node then coroutine.yield(current_node.value) -- yield 当前节点值 table.insert(stack, #stack + 1, current_node.right) -- 右子树后入栈 table.insert(stack, #stack + 1, current_node.left) -- 左子树先入栈 (前序遍历) end end end) end -- 构建一个简单的二叉树 local tree = Tree:new(1, Tree:new(2, Tree:new(4), Tree:new(5)), Tree:new(3, Tree:new(6), Tree:new(7)) ) local iterator = tree:preorder_iterator() print("前序遍历结果:") while true do local status, value = coroutine.resume(iterator, tree) -- 传递根节点 if not status then break end print(value) end
输出结果 (前序遍历顺序):
前序遍历结果: 1 2 4 5 3 6 7
代码解释:
Tree:preorder_iterator() 方法返回一个协同程序迭代器。
迭代器使用栈 (stack) 实现前序遍历。
每次访问一个节点,coroutine.yield(current_node.value) 将节点值 yield 出去。
主程序通过 while 循环和 coroutine.resume 不断驱动迭代器,获取遍历结果。
复杂的系统常常需要状态机来管理不同的状态和状态转换。协同程序可以将每个状态表示为一个函数,利用 yield 在状态之间切换。
代码示例 (简单的交通信号灯状态机):
local TrafficLight = {} function TrafficLight:new() local light = {state = "red"} setmetatable(light, self) self.__index = self return light end function TrafficLight:start() return coroutine.create(function(self) while true do if self.state == "red" then print("红灯") self.state = "green" coroutine.yield() -- 切换到绿灯状态 elseif self.state == "green" then print("绿灯") self.state = "yellow" coroutine.yield() -- 切换到黄灯状态 elseif self.state == "yellow" then print("黄灯") self.state = "red" coroutine.yield() -- 切换回红灯状态 end end end) end local light = TrafficLight:new() local light_coroutine = light:start() for i = 1, 10 do coroutine.resume(light_coroutine, light) -- 每次 resume 切换状态 os.execute("sleep 1") -- 模拟等待时间 end
输出结果 (交通信号灯状态循环):
红灯 绿灯 黄灯 红灯 绿灯 黄灯 红灯 绿灯 黄灯 红灯
代码解释:
TrafficLight:start() 方法创建一个协同程序,负责交通信号灯的状态循环。
while true 循环无限循环,模拟交通信号灯持续工作。
if-elseif-else 结构根据当前状态 self.state 输出灯光颜色,并更新状态。
coroutine.yield() 在每个状态结束后挂起,等待下次 resume 切换到下一个状态。
主程序循环 resume 协同程序,模拟时间流逝,驱动状态机运行。
虽然 Lua 本身不直接支持异步 I/O (标准库 socket 是阻塞的),但协同程序可以用来模拟非阻塞 I/O 的效果,简化异步代码的编写。
代码示例 (模拟异步文件读取):
local function async_read_file(filename) return coroutine.create(function() local file, err = io.open(filename, "r") if not file then coroutine.yield(nil, err) -- yield 错误信息 return end local content = file:read("*all") file:close() coroutine.yield(content, nil) -- yield 文件内容 end) end local reader = async_read_file("example.txt") -- 假设 example.txt 存在 local status, result, err = coroutine.resume(reader) if not status then print("读取文件失败:", err) else print("文件内容:") print(result) end -- 主程序可以继续做其他事情,稍后再次 resume reader 来获取结果 (如果文件还未读取完成,需要更复杂的机制)
代码解释:
async_read_file 函数创建一个协同程序,负责读取文件。
在协同程序内部,文件 I/O 操作仍然是阻塞的,但是 coroutine.yield 允许在 I/O 操作期间挂起协同程序,将控制权交还给主程序。
主程序可以先 resume 启动文件读取,然后继续执行其他任务。
当文件读取完成后 (或者发生错误),协同程序 yield 文件内容或错误信息。
主程序再次 resume 可以获取文件读取的结果。
注意: 这只是一个简单的模拟,真正的异步 I/O 需要操作系统或底层的非阻塞 I/O 支持。 Lua 的 lua-llthreads 库等可以结合操作系统线程和协同程序来实现更完善的异步编程模型。
单线程并发: 协同程序无法利用多核 CPU 的并行处理能力,所有协同程序都运行在同一个线程中。对于 CPU 密集型任务,线程可能更适合。
调试复杂性: 协同程序的执行流程可能比线性代码更复杂,调试时需要理解 yield 和 resume 的切换过程。
错误处理: 协同程序中的错误如果不处理,可能会导致程序崩溃。需要注意使用 pcall 或 xpcall 来捕获协同程序中的错误。
避免长时间阻塞: 在一个协同程序中执行长时间阻塞的操作 (例如阻塞 I/O) 会影响整个 Lua 线程的响应性,包括其他协同程序。应该尽量将阻塞操作移到单独的线程中处理 (如果需要真正的并行)。
yield 上下文: coroutine.yield 只能在协同程序函数体的顶层直接调用,不能在嵌套函数或匿名函数中调用。
Lua 协同程序是一种强大的工具,它提供了一种轻量级、协作式的并发模型,可以简化异步编程、迭代器实现和状态机管理等任务。 理解和掌握协同程序的 API 和使用场景,可以帮助你编写更清晰、更高效的 Lua 代码。 在选择使用协同程序还是线程时,需要根据具体的应用场景和需求权衡利弊。 对于 I/O 密集型、需要模拟并发但又不想引入线程复杂性的场景,协同程序通常是一个不错的选择。