前言
提到lua,就不得不提协程coroutine。coroutine是lua的一种内在机制,为lua提供了原生的异步支持。从用户层面来看,用户不需关心coroutine的内在实现,而只需要用coroutine调度function即可,因此非常方便。
对于一个function而言,coroutine可以将function的代码分片,使得一个function可以分阶段运行。在实现上,function的状态管理会与CPU的机制相似。如果把一个function当做一个任务来看待的话,在coroutine的封装下,这个任务会被分解成多个阶段的子任务。这样,我们就可以把多个任务的子任务相互协调调度,实现更加灵活的功能交互。
因此,本期Lua杂谈,就来小试一抔coroutine的使用吧。
coroutine的基本用法
官方5.3.5版本的coroutine库,提供了如下的接口:
1 | // lcorolib.c |
用户可以通过coroutine.wrap与coroutine.create两种方式封装一个function(任务)。通过wrap封装任务会返回一个纯粹的lua函数(type为function),而用create封装则返回的是一个封装好的线程。
在Lua中,线程thread与协程coroutine的概念内涵有较多相似之处,但我们可以认为,线程是更加宏观广泛的概念,协程则是一种特殊的线程。线程强调的不同的routine之间运行是否独立;而协程强调的则是不同routine之间具有相互co的协作功能。
基于这两种方式调度任务的代码写法大同小异。以下以create封装任务为例,我们一起看看会是怎样的进行——
1 | local function output(co, ...) |
我们首先定义了函数output函数输出当前各线程的状态(普通主线程、协程均可)。在coroutine中封装了以下的功能查看线程信息:
coroutine.status:指定一个线程,返回该线程的状态,可以是suspended(没运行,或被切出)、running(正在运行)、dead(任务完成,或遇到错误)以及normal(正调着另外一个协程)四种之一。coroutine.running:返回当前线程以及是否为主线程的boolean。coroutine.isyieldable:返回当前线程是否具有yield切出功能,如果是普通主线程,或者是不支持yield的C编写的线程,就不能切出。
之后定义了协程co,采用create封装一个任务。封装完之后,就试着跑一下。
我们先看看最后的输出结果:
1 | Info: |
然后再分解运行过程:
- 主线程调用
coroutine.resume(co, 1, 2)开始这个协程,其中1, 2为输入参数,对应任务function里的初始参数a, b。 - 在协程中,把
1, 2相加得到3给ab变量,而后output线程状态:status(co)返回了running表示协程co正在运行,而running与isyieldable则只关心哪个线程运行了它们。在协程里,running会返回协程地址以及false,代表不是主线程;而在主线程里,running会返回主线程地址以及true。同样,在协程里,isyieldable会返回true表示该线程可被yield,而在主线程则不行,为false。后面的结果也都同样。 - 调用
coroutine.yield(ab)切出协程,切回主线程,这一阶段返回的结果为ab。 - 主线程视角下,
coroutine.resume的返回结果为当前协程这一阶段是否没有异常(ok)以及协程yield出来的返回值。因此ok1与ret1则为true跟ab。主线程调用output函数查看各个线程状况,可以看到status(co)为suspended,协程co正在暂停状态,等待下一次resume;而由于在主线程,running与isyieldable分别为true跟false。 - 主线程调用
coroutine.resume(co, 11, 22)继续这个协程。在协程co的视角下,local c, d = coroutine.yield(ab)中的c, d,即为resume它的线程传进来的两个参数,在这里也就是11, 22了。 - 协程
co又调起另一个协程,在另一个协程调用output来看原来协程co的status。嘛,这一步只是为了加一个status == "normal"的例子。 - 把
11 + 22的结果33给cd,然后output线程状态,结果与步骤2相似。而后,再把cd给yield切出去。 - 主线程收到返回值
ok2 = true以及ret2 = 33,而后再output,结果也与步骤4相似。 - 主线程再次
resume协程co,输入参数111, 222给协程中的e, f,协程内部output状态后,最终返回了字符串111222,协程任务结束。此时主线程中调用output(co),可以看到co的状态已经为dead。由于协程co没有发生异常,那么dead就表示协程所有的子任务都结束啦~
至此,整一个coroutine.create的例子已经完成。而对于coroutine.wrap,由于返回的是一个lua函数而非线程,因此需要通过pcall等手段捕获错误,从而不至于断掉主线程运行。有兴趣的同学,可以一探究竟~
coroutine与后端洋葱圈模型
coroutine的重点在于co,在官网上,也有排列组合、生产者——消费者等表现协同任务的例子。不过本文则要搬出我们的老朋友——后端的洋葱圈模型,示意图如下:

后端对于数据规范、安全性等是相当有要求的。比如一个获取数据的请求到达后端,首先都要经过重重关卡检查请求的合法性,而后后端控制器才调用服务取出数据,最后返回时,还得再一个个关卡严查,才能把该带的数据带出去。洋葱圈模型的数据处理流水线,便是如此。
我们以一个例子来试试吧~
1 | local function middleware_header(ctx) |
在这个场景里,我们要处理的数据叫context。context先经过两个middleware中间件header与body,通过两个关卡,才进入到正式的掌权端controller。
在header中间件中,context为自己的header加上了两个头衔:user-agent表示用户代理身份,这里叫做ShangQi;referer表示从哪里来,这里叫做America。
在body中间件中,context为自己的body加上了Chinese Hero的数据,然后切出不管,并且打赌,如果切回来不是Chinese Hero的话,就告诉全世界,这个user-agent身份不能代表Chinese Hero。
但在真正掌握控制大权的controller中,则不会为标榜Chinese Hero的非中国人买单。
1 | ShangQi is not a Chinese Hero! |
那么,怎样实现这一过程呢?我们看一下整一个代码:
1 | local function handle(ctx, ...) |
我们可以把所有的中间件与控制器处理任务都封装成协程,然后入栈(table.insert)。最后一个处理者(handler),也就是控制器处理完毕后,开始将一个个协程pop(table.remove)出来,再resume,这样,就能够模拟洋葱圈模型的操作了。
总结
lua的coroutine非常灵活。配合各种底层架构,可以玩出很多种不同的花样。多上手试试吧~