skynet--lua层消息处理机制

lua层消息处理机制
  1. 协程的概念
    在讨论lua层的消息处理机制之前,首先要了解一个概念,协程。协程可以视为程序的执行单位,和线程不同,线程是抢占式的,多条线程是并行时运行的,而协程则不是,协程是协同式的,比如有三个协程按顺序先后创建coA、coB、coC,那么在没有任意一条协程主动挂起(yield)的情况下,执行顺序则是coA执行完,在执行coB,然后再执行coC。也就是说,除非有协程主动要求挂起,否则必须等当前协程执行完,再去执行下面一个创建的协程。比如说,coA执行完,接着就是执行coB,此时coB挂起,那么直接执行coC,coC执行完以后,如果coB被唤醒了,则接着上次开始阻塞的部分继续执行余下的逻辑。维基百科对协程的定义如下:

引证来源:https://en.wikipedia.org/wiki/Thread_(computing) 【Processes, kernel threads, user threads, and fibers】这一节
Fibers are an even lighter unit of scheduling which are cooperatively scheduled: a running fiber must explicitly "yield" to allow another fiber to run, which makes their implementation much easier than kernel or user threads. A fiber can be scheduled to run in any thread in the same process. This permits applications to gain performance improvements by managing scheduling themselves, instead of relying on the kernel scheduler (which may not be tuned for the application). Parallel programming environments such as OpenMP typically implement their tasks through fibers. Closely related to fibers are coroutines, with the distinction being that coroutines are a language-level construct, while fibers are a system-level construct.

如上所示,协程和纤程十分相似(纤程是线程下的执行单位),区别在于,纤程是操作系统实现的,而协程是语言本身提供。

  1. 协程的使用
    这里引用一篇文档加以说明:

引证来源:http://cloudwu.github.io/lua53doc/manual.html#2.6
Lua 支持协程,也叫 协同式多线程。 一个协程在 Lua 中代表了一段独立的执行线程。 然而,与多线程系统中的线程的区别在于, 协程仅在显式调用一个让出(yield)函数时才挂起当前的执行。

调用函数 coroutine.create 可创建一个协程。 其唯一的参数是该协程的主函数。 create 函数只负责新建一个协程并返回其句柄 (一个 thread 类型的对象); 而不会启动该协程。

调用 coroutine.resume 函数执行一个协程。 第一次调用 coroutine.resume 时,第一个参数应传入 coroutine.create 返回的线程对象,然后协程从其主函数的第一行开始执行。 传递给 coroutine.resume 的其他参数将作为协程主函数的参数传入。 协程启动之后,将一直运行到它终止或 让出。

协程的运行可能被两种方式终止: 正常途径是主函数返回 (显式返回或运行完最后一条指令); 非正常途径是发生了一个未被捕获的错误。 对于正常结束, coroutine.resume 将返回 true, 并接上协程主函数的返回值。 当错误发生时, coroutine.resume 将返回 false 与错误消息。

通过调用 coroutine.yield 使协程暂停执行,让出执行权。 协程让出时,对应的最近 coroutine.resume 函数会立刻返回,即使该让出操作发生在内嵌函数调用中 (即不在主函数,但在主函数直接或间接调用的函数内部)。 在协程让出的情况下, coroutine.resume 也会返回 true, 并加上传给 coroutine.yield 的参数。 当下次重启同一个协程时, 协程会接着从让出点继续执行。 此时,此前让出点处对 coroutine.yield 的调用 会返回,返回值为传给 coroutine.resume 的第一个参数之外的其他参数。

与 coroutine.create 类似, coroutine.wrap 函数也会创建一个协程。 不同之处在于,它不返回协程本身,而是返回一个函数。 调用这个函数将启动该协程。 传递给该函数的任何参数均当作 coroutine.resume 的额外参数。 coroutine.wrap 返回 coroutine.resume 的所有返回值,除了第一个返回值(布尔型的错误码)。 和 coroutine.resume 不同, coroutine.wrap 不会捕获错误; 而是将任何错误都传播给调用者。

下面的代码展示了一个协程工作的范例:

function foo (a)
   print("foo", a)
   return coroutine.yield(2*a)
 end
 
 co = coroutine.create(function (a,b)
       print("co-body", a, b)
       local r = foo(a+1)
       print("co-body", r)
       local r, s = coroutine.yield(a+b, a-b)
       print("co-body", r, s)
       return b, "end"
 end)
 
 print("main", coroutine.resume(co, 1, 10))
 print("main", coroutine.resume(co, "r"))
 print("main", coroutine.resume(co, "x", "y"))
 print("main", coroutine.resume(co, "x", "y"))

当你运行它,将产生下列输出:

co-body 1       10
 foo     2
 main    true    4
 co-body r
 main    true    11      -9
 co-body x       y
 main    true    10      end
 main    false   cannot resume dead coroutine

你也可以通过 C API 来创建及操作协程: 参见函数 lua_newthread, lua_resume, 以及 lua_yield。

这里对lua协程的代码使用,做了充分的说明,对我们理解lua层消息派发十分有帮助

  1. skynet消息处理机制
    在前文,我们已经说明了,一个lua服务在接收消息时,最终会传给lua层的消息回调函数skynet.dispatch_message
-- skynet.lua
function skynet.dispatch_message(...)
	local succ, err = pcall(raw_dispatch_message,...)
	while true do
		local key,co = next(fork_queue)
		if co == nil then
			break
		end
		fork_queue[key] = nil
		local fork_succ, fork_err = pcall(suspend,co,coroutine.resume(co))
		if not fork_succ then
			if succ then
				succ = false
				err = tostring(fork_err)
			else
				err = tostring(err) .. "\n" .. tostring(fork_err)
			end
		end
	end
	assert(succ, tostring(err))
end

消息处理函数,只做两件事情,一件是消费当前消息,另一件则是按顺序执行之前通过调用skynet.fork创建的协程,这里我么只关注处理当前消息的情况raw_dispatch_message

-- skynet.lua
local function raw_dispatch_message(prototype, msg, sz, session, source, ...)
	-- skynet.PTYPE_RESPONSE = 1, read skynet.h
	if prototype == 1 then
        ... -- 暂不讨论,直接忽略
	else
		local p = proto[prototype]    -- 找到与消息类型对应的解析协议
		if p == nil then
			if session ~= 0 then
				c.send(source, skynet.PTYPE_ERROR, session, "")
			else
				unknown_request(session, source, msg, sz, prototype)
			end
			return
		end
		local f = p.dispatch  -- 获取消息处理函数,可以视为该类协议的消息回调函数
		if f then
			local ref = watching_service[source]
			if ref then
				watching_service[source] = ref + 1
			else
				watching_service[source] = 1
			end
			local co = co_create(f)   -- 如果协程池内有空闲的协程,则直接返回,否则创建一个新的协程,该协程用于执行该类协议的消息处理函数dispatch
			session_coroutine_id[co] = session
			session_coroutine_address[co] = source
			suspend(co, coroutine.resume(co, session,source, p.unpack(msg,sz, ...)))  -- 启动并执行协程,将结果返回给suspend
		else
			unknown_request(session, source, msg, sz, proto[prototype].name)
		end
	end
end

消息处理的分为两种情况,一种是其他服务send过来的消息,还有一种就是自己发起同步rpc调用(调用call)后,获得的返回结果(返回消息的类型是PTYPE_RESPONSE)。关于call的情况,后面会详细讨论,现在只讨论如何处理其他服务send过来的消息。
整个执行的流程如下所示:

  • 根据消息的类型,找到对应的先前注册好的消息解析协议
  • 获取一个协程(如果协程池中有空闲的协程,则直接获取,否则重新创建一个),并让该协程执行消息处理协议的回调函数dispatch
  • 启动并执行协程,将协程执行的结果返回给suspend函数,返回结果,就是一个coroutine挂起的原因,这个suspend函数,就是针对coroutine挂起的不同原因,做专门的处理

这里对协程的复用,做一些小小的说明,创建协程的函数,非常有意思,为了进一步提高性能,skynet对协程做了缓存,也就是说,一个协程在使用完以后,并不是让他结束掉,而是把上一次使用的dispatch函数清掉,并且挂起协程,放入一个协程池中,供下一次调用。下次使用时,他将执行新的dispatch函数,只有当协程池中没有协程时,才会去创建新协程,如此循环往复

-- skynet.lua
local function co_create(f)
	local co = table.remove(coroutine_pool)
	if co == nil then  -- 协程池中,再也找不到可以用的协程时,将重新创建一个
		co = coroutine.create(function(...)
			f(...)  -- 执行回调函数,创建协程时,并不会立即执行,只有调用coroutine.resume时,才会执行内部逻辑,这行代码,只有在首次创建时会被调用
			
			-- 回调函数执行完,协程本次调用的使命就完成了,但是为了实现复用,这里不能让协程退出,而是将
			-- upvalue回调函数f赋值为空,再放入协程缓存池中,并且挂起,以便下次使用
			while true do
				f = nil
				coroutine_pool[#coroutine_pool+1] = co
				
				f = coroutine_yield "EXIT"  -- 1
				f(coroutine_yield())  -- 2
			end
		end)
	else
		coroutine.resume(co, f)  -- 唤醒第(1)处代码,并将新的回调函数,赋值给(1)处的upvalue f函数,此时在第(2)个yield处挂起
	end
	return co
end

local function raw_dispatch_message(prototype, msg, sz, session, source, ...)
    ...
    -- 如果是创建后第一次使用这个coroutine,这里的coroutine.resume函数,将会唤醒该coroutine,并将第二个至最后一个参数,传给运行的函数
    -- 如果是一个复用中的协程,那么这里的coroutine.resume会将第二个至最后一个参数,通过第(2)处的coroutine_yield返回给消息回调函数
    suspend(co, coroutine.resume(co, session,source, p.unpack(msg,sz, ...)))
    ...
end

上面的逻辑在完成回调函数调用后,会对协程进行回收,它会将回调函数清掉,并且将当前协程写入协程缓存列表中,然后挂起协程,挂起类型为“EXIT”,如上面的代码所示,对挂起类型进行处理的函数是suspend函数,当一个协程结束时,会进行如下操作

-- skynet.lua
function suspend(co, result, command, param, size)
...
	elseif command == "EXIT" then
		-- coroutine exit
		local address = session_coroutine_address[co]
		release_watching(address)
		session_coroutine_id[co] = nil
		session_coroutine_address[co] = nil
		session_response[co] = nil
...
end

其实这里是将与本协程关联的数据清空,包括向本服务发送消息的服务的地址,session,以及本服务对请求服务返回消息的确认信息。在lua层处理一条消息,本质上是在一个协程里进行的,因此要以协程句柄作为key,保存这些变量。协程每次暂停,都需要使用或处理这些数据,并告知当前协程的状态,以及要根据不同的状态做出相应的处理逻辑,比如当一个协程使用完毕时,就会挂起,并返回“EXIT”类型,意味着协程已经和之前的消息无关系了,需要清空与本协程关联的所有消息相关的信息,以便下一条消息使用。
协程发起一次同步RPC调用(挂起状态类型为“CALL”),或者投入睡眠时(挂起状态类型为“SLEEP”),也会使自己挂起,此时要为当前的协程分配一个唯一的session变量,并且以session为key,协程地址为value存入一个table表中,目的是,当对方返回结果,或者定时器到达时间timer线程向本服务发送一个唤醒原来协程的消息时,能够通过session找到对应的协程,并将其唤醒,从之前挂起的地方继续执行下去。
当一个服务向本服务发起一次call调用时,本服务需要返回一个结果变量给请求者,此时也需要将本协程挂起,向请求者返回结果时,需要调用如下接口

-- skynet.lua
function skynet.ret(msg, sz)
	msg = msg or ""
	return coroutine_yield("RETURN", msg, sz) -- 1
end

function suspend(co, result, command, param, size)
...
	elseif command == "RETURN" then
		local co_session = session_coroutine_id[co]
		local co_address = session_coroutine_address[co]
		if param == nil or session_response[co] then
			error(debug.traceback(co))
		end
		session_response[co] = true
		local ret
		if not dead_service[co_address] then
			ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, param, size) ~= nil
			if not ret then
				-- If the package is too large, returns nil. so we should report error back
				c.send(co_address, skynet.PTYPE_ERROR, co_session, "")
			end
		elseif size ~= nil then
			c.trash(param, size)
			ret = false
		end
		return suspend(co, coroutine.resume(co, ret)) -- 重新唤醒(1)处,此时skynet.ret返回
...
end

在调用skynet.ret以后,调用该接口的协程就会挂起,此时挂起的状态类型是“RETURN”,这里挂起的目的是,等待返回消息的逻辑处理完,再接着执行协程挂起处后面的逻辑。suspend里所做的处理,也就是,将消息插入目的服务的次级消息队列中,然后再唤醒已经挂起的协程。

  1. 对其他服务的call访问
    一个服务,向另一个服务发起同步rpc调用,首先要挂起当前协程,然后是将目的服务发送一个消息,并且在本地记录一个唯一的session值,并以其为key,以挂起的协程地址为value存入一个table中,当目标服务返回结果时,根据这个session找回对应的协程,并且调用resume函数唤醒他。
-- skynet.lua
local function yield_call(service, session)
	watching_session[session] = service
	local succ, msg, sz = coroutine_yield("CALL", session)
	watching_session[session] = nil
	if not succ then
		error "call failed"
	end
	return msg,sz
end

function skynet.call(addr, typename, ...)
	local p = proto[typename]
	local session = c.send(addr, p.id , nil , p.pack(...))
	if session == nil then
		error("call to invalid address " .. skynet.address(addr))
	end
	return p.unpack(yield_call(addr, session))
end

function suspend(co, result, command, param, size)
...
	if command == "CALL" then
		session_id_coroutine[param] = co
...
end

local function raw_dispatch_message(prototype, msg, sz, session, source, ...)
	-- skynet.PTYPE_RESPONSE = 1, read skynet.h
	if prototype == 1 then
		local co = session_id_coroutine[session]
		if co == "BREAK" then
			session_id_coroutine[session] = nil
		elseif co == nil then
			unknown_response(session, source, msg, sz)
		else
			session_id_coroutine[session] = nil
			suspend(co, coroutine.resume(co, true, msg, sz))
		end
	else
	...
	end

上面一段逻辑的流程如下所示:

  • 发起一个同步rpc调用,向目标服务的次级消息队列插入一个消息
  • 挂起当前协程,yield_call里的coroutine_yield("CALL", session)使得当前协程挂起,并在此时suspend执行记录session为key,协程地址为value,将其写入一个table session_id_coroutine中,此时协程等待对方返回消息
  • 当目标服务返回结果时,先根据session找到先前挂起的协程地址,然后通过resume函数唤醒他,此时call返回结果,一次同步rpc调用就结束了。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值