了解协程

新入门skynet系列视频b站网址 https://www.bilibili.com/video/BV19d4y1678X


skynet框架实现中用到了协程。特别是lua应用层在消息调度的时候。

  • 基本概念
  • skynet的协程框架
  • skynet的协程池具体工作原理

协程

每个lua虚拟机可以有很多个协程。说个场景。一个车间为了赶制口罩,于是两班倒的开动机器工作。上白班的工人下班后,上晚班的工作继续干活。上晚班的下班后,上白班的又继续...

也就是上白班是一条线,上晚班是 一条线。他们的特点是不会同时工作,但是工作时共享相同的资源。比如说他们做口罩都是在9527号厂房,使用同样的十台机器。

这里的 一条线 就可以认为是一个协程。一个lua虚拟机中的多个协程协调工作。注意虚拟机中永远只有一个协程在工作

协程都会绑定一个主体函数.如果协程还没有运行过,那么调用coroutine_resume(co,...)就会导致协程开始运行,即开始执行这个主体函数。既然主体函数开始执行,那么主体函数收到的参数就是上面的"..."。调用者此时处于阻塞等待的状态。当主体函数执行完成的时候,coroutine_resume也就返回了,对于调用者来说,coroutine_resume返回值就是主体函数的返回值。

local co = coroutine_create(
    function(money) --绑定了一个匿名主体函数
        
    	-- dosomething
        
        return  "绫罗绸缎" --ret的返回值
    end
)
local ok,ret = coroutine_resume(co,955) --当协程co执行的时候,当前调用者是阻塞的,即不能立即执行第10行
print(ok,ret)

ok是第一个返回值,true表示我们协程内部执行的时候没有报错

又假设,我们开始执行了一个主体函数,在中途,这个主体函数让出了控制权,即调用 coroutine_yield(ret), 那么coroutine_resume会返回。返回值就是ret。

local co = coroutine_create(
    function(money) --绑定了一个匿名主体函数
        
    	-- dosomething 1
        coroutine_yield("感觉快猝死了")
        -- dosomething 2
        
        return  "绫罗绸缎"
    end
)
local ok,ret = coroutine_resume(co,996) --此时ret的值是 "感觉快猝死了"
print(ok,ret)

上面的例子是协程说自己累了,于是让出了控制权,即 挂起协程.但拼命干活本质是一种福报,调用者坚持协程继续开工。这个时候调用者调只需调用 coroutine_resume(co,50)即可让协程继续工作。看下面代码。

local co = coroutine_create(
    function(money) --绑定了一个匿名主体函数
        
    	-- dosomething 1
        local tip = coroutine_yield("感觉快猝死了") --此时的tip就是 50
        print("Much appreciated")
   		-- dosomething 2    
        return  "绫罗绸缎"
    end
)
local ok,ret = coroutine_resume(co,996) --此时ret的值是 "感觉快猝死了"
print(ok,ret)
--建议继续工作
ok,ret = coroutine_resume(co,50) --50是打车费
print(ok,ret) --此时ret的值是 "绫罗绸缎"

注意我们唤醒挂起的协程时,跟第一次执行主体函数一样,也是可以传递参数的。只是协程获取传递进来的参数是在 coroutine_yield函数的左边。

  • coroutine_yield的右边 是当前协程让出时传出去的值,

  • coroutine_yield的左边 是当前协程被唤醒时,外面传进来的值

skynet的协程

  • 大致流程
  • 具体流程

image-20220901095703775

在skynet.lua中大量使用到了协程。

具体的工作流程是怎么样的?

skynet本身是有个协程池的,也就是说如果需要一个协程,那么首先从协程池中去取;如果协程池中没有,那么就创建一个协程。co_create( f ) 就是获取一个协程.注意 f函数不是协程绑定的主体函数。我把f叫做当前协程的任务函数。刚刚获取一个协程时,任务函数并不会马上执行。只有当协程被唤醒的时候,才会执行这个函数。

local coroutine = coroutine --lua自带的协程库

local function coroutine_resume(co, ...) --唤醒协程
	running_thread = co --唤醒协程时 记录当前正在执行的协程
	return coroutine.resume(co, ...)
end
local coroutine_yield = coroutine.yield --挂起协程
local coroutine_create = coroutine.create -- 创建协程

local function co_create(f)
	local co = tremove(coroutine_pool)--从协程池中取出一个协程
	if co == nil then
		co = coroutine_create(function(...)
			f(...)
			while true do
				-- coroutine exit

				-- recycle co into pool
				f = nil
				coroutine_pool[#coroutine_pool+1] = co
				-- recv new main function f
				f = coroutine_yield "SUSPEND"--挂起当前协程 当协程再次唤醒时 把传递进来的参数赋值给f 
				f(coroutine_yield())
			end
		end)
	else
		-- pass the main function f to coroutine, and restore running thread
		local running = running_thread --保存调用co_create所在的协程
		coroutine_resume(co, f) --重新设置任务函数 另外 coroutine_resume每次都会设置当前正在运行的协程
		running_thread = running --恢复记录调用co_create所在的协程
	end
	return co
end

上面的代码看出,co_create是对lua自带协程库包了一层。而调用co_create( f )时指定的 f 函数 并不是这个协程真正绑定的主体函数。真正的绑定的主体函数是匿名函数

function(...) end

这里协程好像是一个人,当你需要完成一个任务时,你先找到一个人,然后给他安排这个任务,这个任务就是f。任务做完了,人就放到协程池里,其实就是挂起。当有其他任务需要人来做时,就去协程池叫一个人,并设置新任务f。

针对上面代码分析。先假设协程池是空的。当我们需要做一个任务时,我们一般

  • 先获取一个协程 co = co_reate(f),这时候会调用 coroutine_create 创建一个协程。此时已经设置好 f 了

  • 调用 coroutine_resume (co,...),这时候协程会运行起来,直到任务完成,就把自己放回协程池,同时调用 coroutine_yield "SUSPEND" 把自己挂起。这时候 coroutine_resume (co,...) 才算是返回了,且返回值中就有 "SUSPEND"。需要注意的是:协程虽然在逻辑上是回收了,但是他是挂起状态。

    coroutine_resume(co,...) 是唤醒指定协程,同时可以给这个协程传递参数。coroutine_resume调用的时候,被唤醒的协程开始执行代码,coroutine_resume此时不会立即返回,直到唤醒的协程挂起。coroutine_yield( ...) 是挂起当前协程。当我们挂起协程的时候,也就是coroutine_resume函数返回的时候。返回参数由coroutine_yield 指定。说多了怕搞混,实际上 coroutine_resume 是开始执行一个协程或者是唤醒一个挂起的协程 。 当你第一次开始执行一个协程,它会从绑定的主体函数处开始运行。 如果协程运行起来没有错误, coroutine_resume 返回 true 加上绑定函数的所有返回值(绑定函数执行结束了)。如果是唤醒一个协程,则返回 true 加上传给 coroutine_yield 的所有值。 如果有任何错误发生, resume 返回 false 加错误消息

假设我们又要做一个新任务,于是我们又调用co_create(f)去获取一个协程。此时就会执行下面的代码

local co = tremove(coroutine_pool)--从协程池中取出一个协程 此时会取出刚刚我们回收的协程

local running = running_thread --running_thread表示调用co_create的协程
coroutine_resume(co, f) --唤醒协程
running_thread = running

return co

此时拿到的协程,就是我们刚刚放回协程池的协程。还记得他的挂起位置码?是在 f = coroutine_yield "SUSPEND"。此时调用调用第四行 coroutine_resume(co, f),就会让协程继续运行。当然运行不会太久。看代码 第八行

while true do
    -- coroutine exit

    -- recycle co into pool
    f = nil
    coroutine_pool[#coroutine_pool+1] = co
    -- recv new main function f
    f = coroutine_yield "SUSPEND"--挂起当前协程 当协程再次唤醒时 把传递进来的参数赋值给f 
    f(coroutine_yield())
end

此时f被替换成新的任务函数 f了,然后 coroutine_yield() 又再次把自己挂起,此时 coroutine_resume(co, new_f)才算返回了。

注意此时协程挂起的位置,说明只要下次被唤醒,就可以执行f函数了。当f函数执行完,协程又会被回收到协程池,并挂起在 第八行。

当我们调用co_create获取到一个协程后,我们后面只要在合适的时机,再次调用coroutine_resume 即可开始执行我们的新任务了。

posted @ 2023-01-03 18:10  程序员阿钢  阅读(325)  评论(0编辑  收藏  举报