lua协程优秀实践:skynet

8 篇文章 1 订阅

前言

本文主要讲解 lua 协程的基本使用,和它在 skynet 这个框架中的实际应用。

skynet简介

可能有的读者学 lua 不是做游戏服务器的,没有了解过 skynet,简单介绍下,skynet 是 云风 大神开发的开源服务器引擎,应用于当下众多的游戏公司的服务器框架。

lua协程是什么

从多线程(multithreading)的角度看,协程(coroutine)与线程(thread)类似:协程是一系列的可执行语句,拥有自己的栈、局部变量和指令指针,同时协程又与其他协程共享了全局变量和其他几乎一切资源。线程和协程的主要区别在于,一个多线程程序可以并行运行多个线程,而协程却需要彼此协作地运行,即在任意指定的时刻只能有一个协程运行,且只有当正在运行的协程显式地被挂起(suspend)时其执行才会暂停。
                                   ----《Lua程序设计第四版》

以上这段话摘自《Lua程序设计第四版》,是lua官方指定的入门书籍哦。“协程是一系列的可执行语句,拥有自己的栈、局部变量和指令指针,同时协程又与其他协程共享了全局变量和其他几乎一切资源” 这句话可以这样简单理解下,lua 中的协程对象,即是一个单独的 lua 虚拟机,所以它很容易做到拥有自己的栈,局部变量和指令指针,然后呢,它又是被主虚拟机(就是创建该协程对象的虚拟机)持有的一个对象而已,主虚拟机创建的 n 个协程对象,和其他数据类型(比如函数,table)的对象一样,共享主虚拟机的资源。

常用API介绍

1. 创建协程:

官方手册:
coroutine.create (f)
创建一个主体函数为 f 的新协程。 f 必须是一个 Lua 的函数。 返回这个新协程,它是一个类型为 “thread” 的对象。

该接口传递协程执行的函数体,返回协程对象,并不会执行函数体。当前协程状态为挂起:

local co = coroutine.create(function() print "hello, im a coroutine" end)
print(coroutine.status(co))

输出:
suspended

2. 唤醒协程:

官方手册:
coroutine.resume (co [, val1, ···])
开始或继续协程 co 的运行。 当你第一次延续一个协程,它会从主体函数处开始运行。 val1, … 这些值会以参数形式传入主体函数。 如果该协程被让出,resume 会重新启动它; val1, … 这些参数会作为让出点的返回值。
如果协程运行起来没有错误, resume 返回 true 加上传给 yield 的所有值 (当协程让出), 或是主体函数的所有返回值(当协程中止)。 如果有任何错误发生, resume 返回 false 加错误消息。

  • 开始协程的运行:
      协程刚创建出来,还没有运行过,此时处于 suspended 状态。resume 协程会让主协程进入 normal 状态,进入协程的函数体执行,该函数体执行时,参数为 resume 调用时跟在协程对象后的所有参数,如果执行成功,则函数体的返回值会跟在 true 后面返回给 resume 调用者,执行失败则返回 false 和错误信息。
local add = function(param1, param2)
    return param1 + param2
end
-- 创建协程 co
local co = coroutine.create(add)
-- 开始协程的运行 co
local ret, val = coroutine.resume(co, 1, 9)
print(ret, val) -- true    10
local ret, val = coroutine.resume(co, 1)
print(ret, val) -- false   cannot resume dead coroutine
co = coroutine.create(add) -- 从上一行打印的错误信息知道该协程状态已经是 dead 了,不能再resume,所以我们重新创建一个新的来测试协程函数体报错的情况
local ret, val = coroutine.resume(co, 1)
print(ret, val) -- false   coro.lua:39: attempt to perform arithmetic on local 'param2' (a nil value)
  • 唤醒主动挂起的协程:
      协程函数体执行结束,状态就会切换成 dead,此后不能再 resume 唤醒协程,所以只能二次唤醒在协程函数体执行过程中调用 coroutine.yield 主动挂起的协程。yield(…) 函数参数取代了上一个例子中 return 关键字后跟着的返回值,作为第一次 resume 调用的返回值,挂起后该协程切换为 suspended 状态,并且将执行权归还主协程,主协程的状态从 normal 切换成 running,主协程再次 resume 唤醒该协程时,resume(co, …) 协程对象之后的所有参数作为 yield 调用的返回值,协程的函数体继续从刚才 yield 调用处往后执行。
      为了传达概念更准确,这里是用以下例子,以及跟上个例子做对比,直接用 “第一次 resume”、 “第二次 resume” 这种贴合例子的描述来讲解,大家可以举一反三,yield 和 resume 的次数可以无限增加的。
local add = function(param1, param2)
    local param3 = coroutine.yield(param1 + param2) -- 这里调用 yield 挂起一次 param1 + param2 的结果返回给第一次resume 调用
    return param1 + param2 + param3 -- 该结果返回给第二次 resume 该协程的调用
end
-- 创建协程
local co = coroutine.create(add)
-- 开始协程的运行 也就是第一次 resume
local ret, val = coroutine.resume(co, 1, 9) -- 参数 1,9 作为 add 的参数;val 为 yield(param1 + param2) 的参数 1 + 9 = 10
print(ret, val) -- true, 10
-- 唤醒主动挂起的协程 co
local ret, val = coroutine.resume(co, 2) -- 参数 2 作为 yield(param1 + param2) 的返回值 val = 2
										 -- 函数体从 yield 处唤醒继续执行到结束,返回值 param1 + param2 + param3 = 12 返回给 resume ,val = 12
print(ret, val) -- true, 12
----------------------------------------------------------------------------------------------------------
co = coroutine.create(add)
local ret, val = coroutine.resume(co, 1) -- 出错返回照旧
print(ret, val) -- false   coro.lua:39: attempt to perform arithmetic on local 'param2' (a nil value)

3. 挂起协程:

官方手册:
coroutine.yield (···)
挂起正在调用的协程的执行。 传递给 yield 的参数都会转为 resume 的额外返回值。

yield 的用法在 resume 的第二个例子中已经讲过了,这里注意一下 yield 是不能在主协程中调用的,强行调用也不会成功,会有如下报错:

attempt to yield across metamethod/C-call boundary

4. 获取协程状态:

官方手册:
coroutine.status (co)
以字符串形式返回协程 co 的状态: 当协程正在运行(它就是调用 status 的那个) ,返回 “running”; 如果协程调用 yield 挂起或是还没有开始运行,返回 “suspended”; 如果协程是活动的,但并不在运行(即它正在延续其它协程),返回 “normal”; 如果协程运行完主体函数或因错误停止,返回 “dead”。

《lua程序设计》对 normal 状态的描述:
当协程 A 唤醒协程 B 时,协程 A 既不是挂起状态(因为不能唤醒协程 A ),也不是运行状态(因为正在运行的协程是 B )。所以,协程 A 此时的状态就被称为正常状态

skynet中的妙用

该协程的处理机制是 skynet lua 服务运转的基础,已经删除掉跟协程处理无关的代码,有兴趣的可以看下我加的注释,就不再赘述了。有什么不明白的地方,或者写的有问题的地方可以留言探讨。

local function co_create(f)
    local co = tremove(coroutine_pool)
    if co == nil then
        co = coroutine_create(function(...)
            --(toby@2022-03-13): 只是该协程第一次被resume的时候会走到这里,先直接调用注册的回调函数
            f(...)
            while true do

                -- ... 这里注释掉了一堆逻辑处理代码 跟协程机制无关

                -- recycle co into pool
                f = nil
                coroutine_pool[#coroutine_pool+1] = co
                --(toby@2022-03-13): 这里的连续两次yield 妙呀
                -- recv new main function f
                --(toby@2022-03-13): 第一次挂起,等待接收新的回调函数
                f = coroutine_yield "SUSPEND"
                --(toby@2022-03-13): 第二次挂起,等待定时器消息来了之后唤醒就可以拿到唤醒时传递进来的参数了
                f(coroutine_yield())
            end
        end)
    else
        -- pass the main function f to coroutine, and restore running thread
        local running = running_thread
        --(toby@2022-03-13): 取到挂起中的工作协程,该协程已经执行完之前的事务,可以将新的回调函数通过 resume 参数传递进去
        coroutine_resume(co, f)
        running_thread = running
    end
    return co
end

结语

最后来个深入了解协程机制的敲门砖,摘自《Lua 程序设计》,Lua 提供了完整的、非对称的协程。可以基于非对称协程实现对称协程。有空了再去瞧瞧吧。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tobybo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值