11.2 协同程序的基本操作 Lua 协同程序 (Coroutines) 基本操作详解 协同程序 (Coroutines) 是 Lua 语言中一种强大的并发编程机制,它允许你在单个线程内实现类似多线程的并发效果,而无需线程切换的开销。协同程序提供了一种协作式多任务处理方式,程序执行权在不同的协同程序之间显式地进行转移。 创建协同程序 (coroutine.create) 启动和恢复协同程序 (coroutine.resume) 挂起协同程序 (coroutine.yield) 检查协同程序状态 (coroutine.status) 包装协同程序 (coroutine.wrap) 判断是否可挂起 (coroutine.isyieldable) 获取当前运行的协同程序 (coroutine.
协同程序 (Coroutines) 是 Lua 语言中一种强大的并发编程机制,它允许你在单个线程内实现类似多线程的并发效果,而无需线程切换的开销。协同程序提供了一种协作式多任务处理方式,程序执行权在不同的协同程序之间显式地进行转移。
创建协同程序 (coroutine.create)
启动和恢复协同程序 (coroutine.resume)
挂起协同程序 (coroutine.yield)
检查协同程序状态 (coroutine.status)
包装协同程序 (coroutine.wrap)
判断是否可挂起 (coroutine.isyieldable)
获取当前运行的协同程序 (coroutine.running)
1. 创建协同程序 (coroutine.create)
协同程序本质上是一个可以被挂起和恢复的函数。coroutine.create 函数负责创建一个新的协同程序。它接受一个函数作为参数,这个函数就是协同程序要执行的代码。coroutine.create 函数本身不会执行传入的函数,而是返回一个新的 thread 类型的值,这个值代表了新创建的协同程序。
代码实践 1:简单的协同程序创建
-- 定义一个协同程序函数 local co_func = function() print("协同程序开始执行") print("协同程序执行结束") end -- 使用 coroutine.create 创建协同程序 local co = coroutine.create(co_func) print("协同程序创建成功,类型为:", type(co)) -- 输出:协同程序创建成功,类型为: thread
内容详解:
我们首先定义了一个名为 co_func 的函数,这个函数包含了协同程序要执行的代码。
coroutine.create(co_func) 调用创建了一个新的协同程序,并将 co_func 函数作为其执行体。
coroutine.create 返回值 co 是一个 thread 类型的值,它代表了我们创建的协同程序。
此时,协同程序 co 仅仅是被创建,并没有开始执行。它的状态处于 suspended (挂起) 状态,等待被启动或恢复。
2. 启动和恢复协同程序 (coroutine.resume)
coroutine.resume 函数用于启动或恢复一个 挂起 的协同程序。当你第一次调用 coroutine.resume 时,它会启动协同程序的执行,从协同程序函数的开始处开始执行。当协同程序执行到 coroutine.yield 或函数结束时,它会挂起或结束执行。后续再次调用 coroutine.resume 时,会从上次挂起的位置继续执行。
coroutine.resume 函数的返回值非常重要:
第一个返回值是状态值: true 表示成功恢复并执行到 yield 或结束; false 表示恢复失败,通常是因为协同程序处于非挂起状态或者发生了错误。
后续返回值是协同程序 yield 传递的值 或 错误信息: 如果恢复成功,且协同程序通过 coroutine.yield 传递了值,则 coroutine.resume 会返回 true 之后跟随这些 yield 的值。如果恢复失败,则 coroutine.resume 会返回 false 之后跟随错误信息。
代码实践 2:启动和恢复协同程序
local co_func = function() print("协同程序开始执行") coroutine.yield() -- 第一次挂起 print("协同程序恢复后继续执行") coroutine.yield(10, 20) -- 第二次挂起并传递值 print("协同程序再次恢复后执行") return "协同程序执行结束" -- 协同程序函数结束 end local co = coroutine.create(co_func) -- 第一次 resume 启动协同程序 local status, err = coroutine.resume(co) print("第一次 resume 状态:", status, err) -- 输出:第一次 resume 状态: true nil -- 此时协同程序执行到第一个 yield 处挂起,输出 "协同程序开始执行" -- 第二次 resume 恢复协同程序 status, err = coroutine.resume(co) print("第二次 resume 状态:", status, err) -- 输出:第二次 resume 状态: true nil -- 此时协同程序从第一个 yield 后继续执行,输出 "协同程序恢复后继续执行",并执行到第二个 yield 处挂起 -- 第三次 resume 恢复协同程序 status, val1, val2, err = coroutine.resume(co) print("第三次 resume 状态:", status, val1, val2, err) -- 输出:第三次 resume 状态: true 10 20 nil -- 此时协同程序从第二个 yield 后继续执行,输出 "协同程序再次恢复后执行",并执行到函数结束,返回 "协同程序执行结束" -- 第四次 resume 恢复协同程序 (此时协同程序已结束) status, err = coroutine.resume(co) print("第四次 resume 状态:", status, err) -- 输出:第四次 resume 状态: false cannot resume dead coroutine -- 此时协同程序已经执行结束 (dead 状态),无法再次恢复,resume 返回 false 和错误信息
内容详解:
第一次 coroutine.resume(co): 启动协同程序 co,程序从 co_func 函数开始执行,打印 "协同程序开始执行",然后遇到 coroutine.yield() 挂起。 coroutine.resume 返回 true (成功) 和 nil (无错误)。
第二次 coroutine.resume(co): 恢复挂起的协同程序 co,程序从上次 yield 的位置继续执行,打印 "协同程序恢复后继续执行",然后遇到 coroutine.yield(10, 20) 挂起并传递值 10 和 20。 coroutine.resume 返回 true 和 nil。
第三次 coroutine.resume(co): 再次恢复协同程序 co,程序从第二个 yield 后继续执行,打印 "协同程序再次恢复后执行",然后执行到函数末尾,返回 "协同程序执行结束"。 coroutine.resume 返回 true, 10, 20 (yield 传递的值), 和 nil。 注意,yield 传递的值会作为 resume 的后续返回值返回。
第四次 coroutine.resume(co): 试图恢复已经执行结束的协同程序 co。由于协同程序已经处于 dead 状态,无法被恢复,coroutine.resume 返回 false 和错误信息 "cannot resume dead coroutine"。
3. 挂起协同程序 (coroutine.yield)
coroutine.yield 函数是协同程序的核心操作之一。它用于 挂起 当前正在执行的协同程序,并将执行权交还给 调用者 (通常是调用 coroutine.resume 的程序)。 coroutine.yield 可以选择性地传递一些值给调用者,这些值会作为 coroutine.resume 的后续返回值返回。
当协同程序被 coroutine.resume 恢复时,coroutine.yield 本身也会返回值,这些值是 coroutine.resume 传递给协同程序的参数。
代码实践 3:yield 的参数和返回值
local co_func = function(init_val) print("协同程序开始执行,初始值:", init_val) local received_val = coroutine.yield(init_val * 2) -- 第一次 yield,传递 init_val * 2 print("协同程序恢复后,接收到的值:", received_val) received_val = coroutine.yield(received_val + 5, "hello") -- 第二次 yield,传递 received_val + 5 和 "hello" print("协同程序再次恢复后,接收到的值:", received_val) return "协同程序最终结束" end local co = coroutine.create(co_func) -- 第一次 resume 启动协同程序,并传递参数 5 local status, yield_val = coroutine.resume(co, 5) print("第一次 resume 状态:", status, "yield 值:", yield_val) -- 输出:第一次 resume 状态: true yield 值: 10 -- 协同程序启动,打印 "协同程序开始执行,初始值: 5",然后 yield 返回 10 -- 第二次 resume 恢复协同程序,并传递参数 20 status, yield_val1, yield_val2 = coroutine.resume(co, 20) print("第二次 resume 状态:", status, "yield 值:", yield_val1, yield_val2) -- 输出:第二次 resume 状态: true yield 值: 25 hello -- 协同程序恢复,打印 "协同程序恢复后,接收到的值: 20",然后 yield 返回 25 和 "hello" -- 第三次 resume 恢复协同程序,并传递参数 30 status, final_result = coroutine.resume(co, 30) print("第三次 resume 状态:", status, "最终结果:", final_result) -- 输出:第三次 resume 状态: true 最终结果: 协同程序最终结束 -- 协同程序恢复,打印 "协同程序再次恢复后,接收到的值: 30",然后函数结束返回 "协同程序最终结束"
内容详解:
第一次 coroutine.resume(co, 5): 启动协同程序并传递参数 5。 co_func 函数接收到 init_val = 5。 coroutine.yield(init_val * 2) 挂起协同程序,并将 10 (5 * 2) 作为 yield 的返回值传递给 resume。 resume 返回 true 和 10。
第二次 coroutine.resume(co, 20): 恢复协同程序并传递参数 20。 协同程序从上次 yield 的位置继续执行。 coroutine.yield 本身返回 20,赋值给 received_val。 coroutine.yield(received_val + 5, "hello") 挂起协同程序,并将 25 (20 + 5) 和 "hello" 作为 yield 的返回值传递给 resume。 resume 返回 true, 25, 和 "hello".
第三次 coroutine.resume(co, 30): 恢复协同程序并传递参数 30。 coroutine.yield 返回 30,赋值给 received_val。 函数执行到末尾,返回 "协同程序最终结束"。 resume 返回 true 和 "协同程序最终结束"。
总结 coroutine.yield 的双向数据传递:
coroutine.yield(...) 从协同程序传递数据给调用者 (resume): yield 传递的参数会作为 coroutine.resume 的后续返回值返回给调用者。
coroutine.resume(co, ...) 从调用者传递数据给协同程序 (yield): resume 的后续参数会作为 coroutine.yield 本身的返回值返回给协同程序。
4. 检查协同程序状态 (coroutine.status)
coroutine.status 函数用于检查一个协同程序的状态。它接受一个协同程序对象作为参数,并返回一个描述协同程序状态的字符串。Lua 协同程序有以下几种状态:
"running": 协同程序正在运行 (只有在协同程序内部调用 coroutine.status 自身时才会出现此状态)。
"suspended": 协同程序被挂起,等待被 coroutine.resume 恢复。 这是协同程序刚创建后的初始状态,以及执行 coroutine.yield 后的状态。
"normal": 协同程序关联的函数已经返回,但协同程序本身还没有结束。这种情况通常发生在协同程序函数调用了其他函数,并且这些函数中包含了 yield 操作,导致主协同程序函数返回后,内部的协同程序仍然处于挂起状态。 注意:在 Lua 5.2 及之后的版本中,"normal" 状态已经移除,统一归为 "suspended" 状态。 在 Lua 5.1 中可能存在 "normal" 状态。
"dead": 协同程序已经执行结束,无法再被恢复。 这是协同程序函数执行完毕或发生错误导致程序终止后的状态。
代码实践 4:检查协同程序状态
local co_func = function() print("协同程序开始执行") coroutine.yield() print("协同程序恢复后执行") return "协同程序执行结束" end local co = coroutine.create(co_func) print("初始状态:", coroutine.status(co)) -- 输出:初始状态: suspended coroutine.resume(co) print("第一次 resume 后状态:", coroutine.status(co)) -- 输出:第一次 resume 后状态: suspended coroutine.resume(co) print("第二次 resume 后状态:", coroutine.status(co)) -- 输出:第二次 resume 后状态: dead
内容详解:
coroutine.create(co_func) 后: 协同程序 co 处于 "suspended" 状态,等待被启动。
第一次 coroutine.resume(co) 后: 协同程序执行到 coroutine.yield() 挂起,状态仍然是 "suspended"。
第二次 coroutine.resume(co) 后: 协同程序执行完毕,函数返回,状态变为 "dead"。
5. 包装协同程序 (coroutine.wrap)
coroutine.wrap 函数是 coroutine.create 的一种简化形式。它也接受一个函数作为参数,并返回一个函数。 不同于 coroutine.create 返回协同程序对象,coroutine.wrap 返回的是一个函数。 每次调用 coroutine.wrap 返回的函数时,都会 自动 resume 对应的协同程序。
coroutine.wrap 返回的函数在每次调用时,行为类似于 coroutine.resume,但它 不会返回状态值,只返回 coroutine.yield 传递的值 (如果有)。 如果协同程序执行过程中发生错误,错误信息会直接被 抛出,而不是像 coroutine.resume 那样作为返回值返回。
代码实践 5:使用 coroutine.wrap
local co_func = function(start_val) print("协同程序开始执行,初始值:", start_val) local val = coroutine.yield(start_val + 10) print("协同程序恢复后,接收到的值:", val) return "协同程序执行结束" end local wrapped_co = coroutine.wrap(co_func) -- 第一次调用 wrapped_co 函数,相当于第一次 resume local yield_val = wrapped_co(5) print("第一次 wrapped_co 调用,yield 值:", yield_val) -- 输出:第一次 wrapped_co 调用,yield 值: 15 -- 协同程序启动,打印 "协同程序开始执行,初始值: 5",然后 yield 返回 15 -- 第二次调用 wrapped_co 函数,相当于第二次 resume yield_val = wrapped_co(20) print("第二次 wrapped_co 调用,yield 值:", yield_val) -- 输出:第二次 wrapped_co 调用,yield 值: 协同程序执行结束 -- 协同程序恢复,打印 "协同程序恢复后,接收到的值: 20",然后函数结束返回 "协同程序执行结束" -- 第三次调用 wrapped_co 函数 (此时协同程序已结束) -- 再次调用 wrapped_co 会导致错误,因为协同程序已经 dead -- 为了避免错误,通常在使用 wrap 时,要确保协同程序不会被过度 resume -- 在这个例子中,再次调用 wrapped_co 会抛出错误,程序会终止 -- (取消注释以下代码会报错) -- yield_val = wrapped_co(30) -- print("第三次 wrapped_co 调用,yield 值:", yield_val)
内容详解:
local wrapped_co = coroutine.wrap(co_func) 创建了一个包装函数 wrapped_co,它内部关联了协同程序 co_func。
wrapped_co(5): 调用 wrapped_co 相当于调用 coroutine.resume 启动协同程序并传递参数 5。 wrapped_co 返回 coroutine.yield(start_val + 10) 的值 15。
wrapped_co(20): 再次调用 wrapped_co 相当于再次 resume 协同程序并传递参数 20。 wrapped_co 返回协同程序函数的返回值 "协同程序执行结束"。
第三次调用 wrapped_co(30) (注释掉的代码): 由于协同程序已经执行结束,再次调用 wrapped_co 会尝试 resume 一个 dead 的协同程序,这会导致错误被抛出。
coroutine.wrap 的适用场景:
coroutine.wrap 更适合用于 迭代器 或 生成器 类型的协同程序,你只需要简单地调用返回的函数来逐步获取协同程序 yield 的值,而不需要显式地处理 resume 和状态检查。
6. 判断是否可挂起 (coroutine.isyieldable)
coroutine.isyieldable 函数用于判断当前的执行环境是否允许挂起 (即是否在协同程序内部)。它返回一个布尔值:
true: 当前环境允许挂起,即在协同程序函数内部。
false: 当前环境不允许挂起,即不在协同程序函数内部。
代码实践 6:使用 coroutine.isyieldable
local co_func = function() print("在协同程序内部,是否可挂起:", coroutine.isyieldable()) -- 输出:在协同程序内部,是否可挂起: true coroutine.yield() end local co = coroutine.create(co_func) print("在协同程序外部,是否可挂起:", coroutine.isyieldable()) -- 输出:在协同程序外部,是否可挂起: false coroutine.resume(co)
内容详解:
在 co_func 内部,coroutine.isyieldable() 返回 true,因为在协同程序内部是可以调用 coroutine.yield 进行挂起的。
在协同程序外部 (主程序环境),coroutine.isyieldable() 返回 false,因为在主程序环境中直接调用 coroutine.yield 会导致错误。 coroutine.yield 只能在协同程序内部调用。
coroutine.isyieldable 的应用场景:
coroutine.isyieldable 可以用于在函数库或模块中,判断当前是否在协同程序环境中,从而决定是否允许使用 coroutine.yield 或执行其他协同程序相关的操作。这可以提高代码的健壮性和灵活性。
7. 获取当前运行的协同程序 (coroutine.running)
coroutine.running 函数返回当前正在运行的协同程序对象。如果在主线程环境中调用,它返回 nil。
代码实践 7:使用 coroutine.running
local co1 local co2 co1 = coroutine.create(function() print("co1 正在运行,当前运行的协同程序:", coroutine.running()) -- 输出:co1 正在运行,当前运行的协同程序: thread: 0x... (co1 的地址) co2 = coroutine.create(function() print("co2 正在运行,当前运行的协同程序:", coroutine.running()) -- 输出:co2 正在运行,当前运行的协同程序: thread: 0x... (co2 的地址) print("co2 正在运行,co1 的状态:", coroutine.status(co1)) -- 输出:co2 正在运行,co1 的状态: suspended end) coroutine.resume(co2) -- 在 co1 中启动 co2 end) print("主线程环境,当前运行的协同程序:", coroutine.running()) -- 输出:主线程环境,当前运行的协同程序: nil coroutine.resume(co1)
内容详解:
在主线程环境中调用 coroutine.running() 返回 nil,因为主线程本身不是协同程序。
在 co1 协同程序内部调用 coroutine.running() 返回 co1 自身的对象。
在 co2 协同程序内部调用 coroutine.running() 返回 co2 自身的对象。
在 co2 运行时,co1 处于挂起状态 (suspended)。
coroutine.running 的应用场景:
coroutine.running 可以用于协同程序自身获取自身的引用,例如在某些复杂的协同程序控制逻辑中,协同程序需要知道自身是谁,或者需要将自身的信息传递给其他部分。
总结:Lua 协同程序的基本操作
通过本文的详细讲解和代码实践,我们学习了 Lua 协同程序的基本操作:
coroutine.create(f): 创建一个新的协同程序,以函数 f 为执行体。
coroutine.resume(co, ...): 启动或恢复协同程序 co 的执行,并可以传递参数给协同程序。
coroutine.yield(...): 挂起当前协同程序的执行,并将执行权交还给调用者,同时可以传递值给调用者。
coroutine.status(co): 检查协同程序 co 的状态 ("running", "suspended", "dead")。
coroutine.wrap(f): 创建一个包装函数,每次调用该函数都会自动 resume 对应的协同程序。
coroutine.isyieldable(): 判断当前环境是否允许挂起 (是否在协同程序内部)。
coroutine.running(): 获取当前正在运行的协同程序对象。
掌握这些基本操作是理解和应用 Lua 协同程序的关键。协同程序为 Lua 提供了强大的并发编程能力,可以用于实现各种高级特性,例如:
迭代器和生成器: 使用 coroutine.yield 可以方便地创建自定义的迭代器和生成器。
协作式多任务: 在单线程环境中模拟并发执行,提高程序的响应性和效率。
状态机: 使用协同程序来管理复杂的状态转换逻辑。
异步编程: 尽管 Lua 是单线程的,但协同程序可以模拟异步操作,例如非阻塞 I/O。
希望本文能帮助你深入理解 Lua 协同程序的基本操作,并在实际开发中灵活运用。