协程的挂起与回复
lua-nginx-module使用Lua拓展Nginx功能的一个优点就是用同步的方式写代码,实现异步的功能。典型的一个API就是ngx.sleep。在C语言中如果调用sleep会使整个线程休眠,对于Nginx这样单进程异步处理流程来说是不可以接受的,要实现将某个请求延迟处理,需要很多额外的代码,增加了开发的难度,而在ngx_lua中ngx.sleep只会暂停当前的协程,不影响其他的协程工作。从这方面看协程更像是用户态线程的简化。
Lua主要作为嵌入式编程语言,只提供了基础的功能,并没有golang中那样对并发原生的支持,对于sleep,socket等的处理都需要开发者来实现,这里以sleep为例。
ngx.sleep实现
Lua提供了两个C语言接口,lua_yield可以将一个协程挂起,lua_resume使协程恢复运行。要使协程休眠一段时间后再运行,可以通过下面的步骤实现。
- 1.添加定时器,一段时间后执行回调函数
- 2.调用lua_yield挂起协程
- 3.在回调函数中调用lua_resume运行挂起的协程
在Lua中调用ngx.sleep(4)时,最终执行的是ngx_http_test_ngx_sleep,如下所示,主要功能是利用ngx_add_timer设置一个定时器,超时后执行ngx_http_test_sleep_handler。
1 |
|
协程的挂起
在ngx_http_test_handler中调用lua_resume(L, 0)执行Lua脚本,如果执行完成返回值为0,这里ngx.sleep会导致lua_yield的调用,这是lua_resume的返回值为1,因此需要判断lua_resume的返回值。
- 返回值为0时,脚本执行结束,返回NGX_OK或NGX_DECLINED
- 返回值为1时,协程被挂起,返回NGX_DONE,Nginx会暂停当前请求的处理
- 返回其它值时,脚本执行出错。
原先的逻辑中直接调用主协程执行lua代码,这里有可能出现协程的挂起,表明当前的Lua代码没有执行完毕,这就需要对每个请求,创建单独的协程进行处理,保证多个并发请求可以同时处理。
GC的影响
Lua中GC采用标记清除的方式,每个变量必须有其他变量引用,否则就可能被GC回收掉。Lua中的协程也是一个GC对象,多个协程同时存在时,必须为每个协程添加引用,以免被回收掉。
仿照lua-nginx-module的做法,在注册表中创建了一个table。
1 |
|
创建协程并通过luaL_ref添加到table中
1 |
|
协程不再需要时从table中删掉
1 |
|
协程的恢复运行
定时器到期后,Nginx会调用ngx_http_test_sleep_handler,从这里开始,继续处理请求。
Nginx为了便于异步处理,将请求的处理分了多个阶段,按照阶段的次序依次处理。逻辑在ngx_http_core_run_phases中,在ngx_http_test_module处理完成后需要交由下一阶段继续处理,为了保持依阶段处理的逻辑,这里不在ngx_http_test_sleep_handler中直接调用lua_resume继续协程运行,而是调用ngx_http_core_run_phases, 这就导致了ngx_http_test_handler回调函数的第二次调用。
1 |
|
为了进行区分,在模块的上下文中增加成员entered_access_phase,用来标志是否是回调函数的第一次调用。如果entered_access_phase,直接调用ctx->resume_handler执行即可,不需要再新建协程。
1 |
|
实现ngx.sleep的完整代码
1 |
|
nginx.conf配置
1 |
|
即可实现对到来的请求,延迟4s后返回403.
备注
这里的代码主要目的是显示完整的流程,所以很多地方没有做错误处理。对于lua_resume调用也没有做协程栈的恢复,这些在实际编程中都是必不可少的。