Skip to content

协程与线程的比较 ​

协程常被称为"用户态线程",但它们与操作系统线程有本质区别。理解这些区别,才能明白什么场景该用协程、什么场景必须用线程。

1. 核心区别一览 ​

对比维度Lua 协程操作系统线程
调度方式协作式:仅在 yield 处主动让出抢占式:由操作系统随时打断切换
并行能力无:同一时刻只有一个协程在运行有:多线程可真正并行跑在多核上
切换开销极小:用户态切换,只保存协程栈较大:陷入内核,保存完整上下文
内存占用每个协程只需很小的栈(KB 级)每个线程默认保留 MB 级的栈空间
数量级轻松创建数万乃至数十万个通常几百上千个就会遇到瓶颈
数据同步无真正并行,一般不需要加锁必须用锁、信号量等同步原语
阻塞影响yield 不影响其他协程,但阻塞调用(如 io.read)会卡住整个状态机一个线程阻塞不影响其他线程
适用场景迭代器、生产者-消费者、异步 I/O 封装、游戏帧逻辑CPU 密集型并行计算、真正的多任务

2. 协作式意味着什么 ​

协程的切换点完全由代码决定:只有执行到 coroutine.yield,控制权才会交出去。因此两个协程的输出顺序是确定的、可复现的:

lua
local function worker(name)
    for i = 1, 2 do
        print(name, "step", i)
        coroutine.yield()      -- 唯一可能被打断的位置
    end
end

local coA = coroutine.create(worker)
local coB = coroutine.create(worker)
coroutine.resume(coA, "A")   -- A step 1
coroutine.resume(coB, "B")   -- B step 1
coroutine.resume(coA)        -- A step 2
coroutine.resume(coB)        -- B step 2
-- 输出
-- A	step	1
-- B	step	1
-- A	step	2
-- B	step	2

线程则随时可能被操作系统打断,同样的代码若用两个线程执行,输出顺序是不确定的。确定性让协程代码更容易调试和推理,但也意味着协程不能利用多核。这是协作式换取的代价。

3. 阻塞调用是协程的天敌 ​

协程的"并发"只存在于 Lua 世界内部。一旦协程里调用了真正阻塞的操作(读文件、网络请求、os.execute 等),整个进程都会被卡住,其他协程毫无办法:

lua
local co = coroutine.create(function()
    -- io.read() 这类阻塞调用会卡住整个程序,其他协程也无法运行
    local line = "simulated blocking result"
    coroutine.yield(line)
end)

print(coroutine.resume(co))   -- 输出 true	simulated blocking result

正因为如此,基于协程的异步框架(如 OpenResty/ngx_lua)都把网络 I/O 做成"非阻塞 + 协程挂起"的组合:请求发出后协程 yield 让出,数据到达后再由框架 resume 恢复协程,从协程的使用者视角看依然是顺序代码。

4. 不需要锁的"并发" ​

因为同一时刻最多只有一个协程在运行,两个协程之间的数据访问天然是串行的。只要不在 yield 调用之间穿插共享状态的修改,就不会出现竞态条件。线程则不同,对共享数据的每次访问都可能与其他线程交错,必须靠锁来保护。

当然,如果宿主程序把同一个 Lua 状态机交给多个线程同时访问,Lua 状态机本身不是线程安全的,仍然需要外部加锁或为每个线程分配独立的状态机(Lua 4.0 起支持的 multiple states 就是为这类场景设计的)。

5. 什么时候用协程,什么时候用线程 ​

选择依据可以归纳为一句话:要并行用线程,要结构清晰用协程。

  • 用协程:任务以等待 I/O 为主、需要把异步回调改写成顺序代码、需要生成器和惰性求值、单机游戏逻辑分帧处理。
  • 用线程:任务以 CPU 计算为主且机器多核、需要真正的并发吞吐、调用的库本身会阻塞。

在嵌入式和游戏领域,最常见的组合是"少量系统线程 + 每个线程里跑一个 Lua 状态机 + 状态机内部用协程做逻辑调度",两者互为补充。

结语 ​

协程与线程解决的是两个不同层面的问题:线程解决"如何同时执行",协程解决"如何把交替执行写成清晰的顺序代码"。Lua 把协程做成语言内建,为的是给宿主程序一个可控的、确定性的多任务模型,切换只在用户态发生。

Hello Lua