前言
提到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非常灵活。配合各种底层架构,可以玩出很多种不同的花样。多上手试试吧~