You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
local i = 0
function receive(prod)
i = i + 1
local status, value = coroutine.resume(prod, i)
return value
end
function send(x)
return coroutine.yield(x)
end
function producer()
return coroutine.create(function()
while true do
local x = io.read()
local r = send(x)
io.write(i,":\r\n")
end
end)
end
function consumer(prod)
while true do
local obtain = receive(prod)
if obtain then
io.write(obtain, "\n\n")
else
break
end
end
end
io.write(i+1, ":\r\n")
p = producer()
consumer(p)
ngx_lua 中协程的使用
Nginx 在 postconfiguration 阶段执行
lua-nginx-module
模块初始化函数ngx_http_lua_init
, 该函数会调用ngx_http_lua_init_vm
来创建和初始化一个 lua 虚拟机环境,由 lua APIluaL_newstate
实现,该接口函数会创建一个协程作为主协程,返回 lua_state(存放堆栈信息,包括后续的请求数据(ngx_http_request_t
),API 注册表等数据都是存放在这里,供 lua 层使用),然后调用ngx_http_lua_init_globals
,该函数做了两件事:global_state
数据结构,这个结构体保存全局相关的一些信息,主要是所有需要垃圾回收的对象。ngx_http_lua_inject_ngx_api
,注册各种 Nginx 层面的 API 函数,设置字符串ngx
为表名,lua 代码中就可以使用ngx.*
来调用这些 API 了。另外,还会调用函数
ngx_http_lua_init_registry
,ngx_http_lua_ctx_tables
就是在这里注册到 Nginx 内存的(lua 中没有引用的变量会被 GC 掉),用来存放单个请求的 ctx 数据(table),即ngx.ctx
。所以,与ngx.var
不一样,ngx.ctx
其实是 lua table,只是在 Nginx 内存中添加了引用。也就不难理解,ngx.ctx
生命周期是在单个 location,因为内部跳转时,会清除对应的 ctx table。要想在父子请求间共享 ngx.ctx,可以参考这篇文章,过程大概是,将对应的 ctx 再次插入 ngx_http_lua_ctx_tables,创建新的索引,索引保存在 ngx.var 中,在子请求时取出重新赋值给 ngx.ctx。master fork worker 进程时,Lua 虚拟机自然也被复制(COW)到了 worker 进程。
请求是在 worker 进程内处理的,处理共分为 11 个阶段,其中在 balancer_by_lua, header_filter, body_filter, log 阶段中,直接在主协程中执行代码,而在 rewrite_by_lua, access_by_lua 和 content_by_lua 阶段中,会创建一个新的协程(boilerplate "light thread" are also called "entry threads")去执行此阶段的 lua 代码。这些新的子协程相互独立,数据隔离,但是共享
global_state
。为什么 content 等几个阶段的处理要在子协程里面处理呢?原因可能是 content 等阶段,需要调用
ngx.sleep
,ngx.socket
I/O 之类的阻塞操作,使用协程实现异步,提高执行效率。如果放在主协程,这类操作就会阻塞主协程,导致 worker 进程无法处理其它请求。ngx.socket
,ngx.sleep
等 API 都会有挂起协程的操作,只能在子协程调用,因此,这些 API 不能在 header_filter 等阶段(主协程)使用。我们知道,协程是非抢占式的,也就是说只有正在运行的协程只有在显式调用 yield 函数后才会被挂起,因此,同一时间内,只有一个协程在处理(因为 worker 是单线程的),lua 协程还有一个特性,就是子协程优先运行,只有当子协程都被挂起或运行结束才会继续运行父协程。
ngx_lua 协程的调度可以参考下面这张图(图片来自):
lua_resume
就是恢复对应的协程运行,在请求处理时,还可能调用 APIngx.thread
来创建light thread
, 可以认为是一种特殊的 lua 协程,没有本质区别,不同的是,它是由ngx_lua
模块进行调度的(详见下面的ngx_http_lua_run_thread
源码)。在需要访问第三方服务时,并发执行,可以减少等待处理时间。从上面可知,在 ngx_lua 内有三层协程 —— 全局的主协程,请求阶段的子协程,以及用户创建的
light thread
,它们分别为父子关系,记住这三个协程代称,后面将会用到。使用
ngx.exit
,ngx.exec
,ngx.redirect
可以直接跳出协程,而不用等待子协程处理完成。light thread
)被挂起light thread
都结束)以上,就是协程在
ngx_lua
模块中的使用与调度。那么 lua 协程到底是个什么神奇的东西呢?lua 协程
Lua 所支持的协程全称被称作协同式多线程(collaborative multithreading),由用户(lua 虚拟机)自己负责管理,在线程内运行,操作系统是感知不到的。特性就如上面所说,主要两条:
是不是很像回调?因此,lua 协程之间不存在资源竞争,也就不需要锁了。严格来说,这种协程只是为了实现异步,而不是并发。而且,lua 是没有线程概念的,lua 语言的定位就是系统嵌入式脚本,由 C 语言调度使用的,在 C 层面创建线程就行了,也使得 lua 更加简单。
yield
的参数值(b1 ... bn)resume
的参数(a1 ... an)使用 lua 协程实现生产者-消费者问题:
从这里可以看到,lua 协程跟线程差别很大,更像是回调,new_thread 只是在内存新建了一个 stack 用于存放新 coroutine 的变量,也称作
lua_State
。小结
OpenResty 将 lua 嵌入到 Nginx 系统,使 Nginx 拥有了 lua 的能力,大大的扩展了 Nginx 系统的开发灵活性和开发效率。达到了以同步的方式写代码,实现异步功能的效果。不用担心异步开发中的顺序问题,又因为单线程的,也不用担心并发开发中最头痛的竞争问题。比起原生的 Nginx 第三方模块开发,开发更简单,系统也更稳定。
需要注意的是,ngx_lua 并没有提高 Nginx 的并发能力,Nginx worker 本来就是使用回调机制来异步处理多个请求的, 当前请求处理阻塞时,会注册一个事件,然后去处理新的请求,从而避免进程因为某个请求阻塞而干等着(参考知乎问答)。
The text was updated successfully, but these errors were encountered: