skynet框架应用 (六) 服务调度

6 服务调度

local skynet = require "skynet"
--让当前的任务等待 time * 0.01s 。
skynet.sleep(time)--启动一个新的任务去执行函数 func , 其实就是开了一个协程,函数调用完成将返回线程句柄
--虽然你也可以使用原生的coroutine.create来创建协程,但是会打乱skynet的工作流程
skynet.fork(func, ...)--让出当前的任务执行流程,使本服务内其它任务有机会执行,随后会继续运行。
skynet.yield()--让出当前的任务执行流程,直到用 wakeup 唤醒它。
skynet.wait()--唤醒用 wait 或 sleep 处于等待状态的任务。
skynet.wakeup(co)--设定一个定时触发函数 func ,在 time * 0.01s 后触发。
skynet.timeout(time, func)--返回当前进程的启动 UTC 时间(秒)。
skynet.starttime()--返回当前进程启动后经过的时间 (0.01) 。
skynet.now()--通过 starttime 和 now 计算出当前 UTC 时间(秒)。
skynet.time()    

6.1 使用sleep休眠

示例代码:testsleep.lua

local skynet = require "skynet"
​
skynet.start(function ()
    skynet.error("begin sleep")
    skynet.sleep(500)   
    skynet.error("begin end")
end)

运行结果(还是先运行main.lua):

$ ./skynet examples/config
testsleep                       #输入testsleep
[:01000010] LAUNCH snlua testsleep
[:01000010] begin sleep
#执行权限已经被第一个服务testsleep给使用了,这里输入个新的服务test,并不会马上就启动服务
test        
[:01000010] begin end       
[:01000012] LAUNCH snlua test  #等第一个服务完成任务了,才启动新的服务
[:01000012] My new service

​ 在console服务中输入testsleep之后,马上再输入test,会发现,test服务不会马上启动,因为这个时候 console正在忙于第一个服务testsleep初始化,需要等待5秒钟之后,输入的test 才会被console处理。

注意: 以上做法是不正确的,在skynet.start函数中的服务初始化代码不允许有阻塞函数的存在,服务的初始化要求尽量快的执行完成,所有的业务逻辑代码不会写在skynet.start 里面。

6.2 在服务中开启新的线程

在skynet的服务中我们可以开一个新的线程来处理业务(注意这个的线程并不是传统意义上的线程,更像是一个虚拟线程,其实是通过协程来模拟的)。

示例代码: testfork.lua

local skynet = require "skynet"function task(timeout)
    skynet.error("fork co:", coroutine.running())
    skynet.error("begin sleep")
    skynet.sleep(timeout)
    
    skynet.error("begin end")
end
​
skynet.start(function ()
    skynet.error("start co:", coroutine.running())
    skynet.fork(task, 500)  --开启一个新的线程来执行task任务
    --skynet.fork(task, 500)  --再开启一个新的线程来执行task任务
end)

运行结果:

$ ./skynet examples/config
testfork                        #输入testfork
[:0100000a] LAUNCH snlua testfork   
[:0100000a] start thread: 0x7f684d967568 false #不同的两个协程
[:0100000a] fork  thread: 0x7f684d969f68 false
[:0100000a] begin sleep

​ 可以看到在testfork启动后,consloe服务仍然可以接受终端输入的test,并且启动。

​ 以后如果遇到需要长时间运行,并且出现阻塞情况,都要使用skynet.fork在创建一个新的线程(协程)。

查看源码skynet.lua了解底层实现,其实就是使用coroutine.create实现

​ 每次使用skynet.fork其实都是从协程池中获取未被使用的协程,并把该协程加入到fork队列中,等待一个消息调度,然后会依次把fork队列中协程拿出来执行一遍,执行结束后,会把协程重新丢入协程池中,这样可以避免重复开启关闭协程的额外开销。

6.3 长时间占用执行权限的任务

示例代码:busytask.lua

local skynet = require "skynet"function task(name)
    local i = 0
    skynet.error(name, "begin task")
    while ( i < 200000000)
    do
        i = i+1
    end
    skynet.error(name, "end task", i)
end
​
skynet.start(function ()
    skynet.fork(task, "task1")
    skynet.fork(task, "task2")
end)

运行结果:

$ ./skynet examples/config
busytask
[:01000010] LAUNCH snlua busytask
[:01000010] task1 begin task       --先运行task1
[:01000010] task1 end task 200000000
[:01000010] task2 begin task        --再运行task2
[:01000010] task2 end task 200000000


​ 上面的运行结果充分说明了,skynet.fork 创建的线程其实通过lua协程来实现的,即一个协程占用执行权后,其他的协程需要等待。

6.4 使用skynet.yield让出执行权

示例代码:testyield.lua

local skynet = require "skynet"function task(name)
    local i = 0
    skynet.error(name, "begin task")
    while ( i < 200000000)
    do
        i = i+1
        if i % 50000000 == 0 then
            skynet.yield()
            skynet.error(name, "task yield")
        end
    end
    skynet.error(name, "end task", i)
end
​
skynet.start(function ()
    skynet.fork(task, "task1")
    skynet.fork(task, "task2")
end)

运行结果:

$ ./skynet examples/config
testyield
[:01000010] LAUNCH snlua testyield
[:01000010] task1 begin task
[:01000010] task2 begin task
[:01000010] task1 task yield
[:01000010] task2 task yield
[:01000010] task1 task yield
[:01000010] task2 task yield
[:01000010] task1 task yield
[:01000010] task2 task yield
[:01000010] task1 task yield
[:01000010] task1 end task 200000000
[:01000010] task2 task yield
[:01000010] task2 end task 200000000

​ 通过使用skynet.yield() 然后同一个服务中的不同线程都可以得到执行权限。

6.5 线程间的简单同步

同一个服务之间的线程可以通过,skynet.wait以及skynet.wakeup来同步线程

示例代码:testwakeup.lua

local skynet = require "skynet"
local cos = {}function task1()
    skynet.error("task1 begin task")
    skynet.error("task1 wait")
    skynet.wait()               --task1去等待唤醒
    --或者skynet.wait(coroutine.running())
    skynet.error("task1 end task")
end
​
​
function task2()
    skynet.error("task2 begin task")
    skynet.error("task2 wakeup task1")
    skynet.wakeup(cos[1])           --task2去唤醒task1,task1并不是马上唤醒,而是等task2运行完
    skynet.error("task2 end task")
end
​
​
skynet.start(function ()
    cos[1] = skynet.fork(task1)  --保存线程句柄
    cos[2] = skynet.fork(task2)
end)

运行结果:

$ ./skynet examples/config
testwakeup
[:01000010] LAUNCH snlua testwakeup
[:01000010] task1 begin task
[:01000010] task2 begin task
[:01000010] task1 wait
[:01000010] task2 wakeup task1
[:01000010] task2 end task
[:01000010] task1 end task

6.6 定时器的使用

​ skynet中的定时器,其实是通过给定时器线程注册了一个超时时间,并且占用了一个空闲协程,空闲协程也是从协程池中获取,超时后会使用空闲协程来处理超时回调函数。

6.6.1 启动一个定时器
示例代码:testtimeout.lua

local skynet = require "skynet"function task()
    skynet.error("task", coroutine.running())
end
​
skynet.start(function ()
     skynet.error("start", coroutine.running())
    skynet.timeout(500, task) --5秒钟之后运行task函数,只是注册一下回调函数,并不会阻塞
end)

运行结果:

$ ./skynet examples/config
testtimeout
[:01000010] LAUNCH snlua testtimeout
[:01000010] task

6.6.2 skynet.start源码分析

​ 其实skynet.start服务启动函数实现中,就已经启动了一个timeout为0s的定时器,来执行通过skynet.start函数传参得到的初始化函数。其目的是为了让skynet工作线程调度一次新服务。这一次服务调度最重要的意义在于把fork队列中的协程全部执行一遍。

6.6.3 循环启动定时器

local skynet = require "skynet"function task()
    skynet.error("task", coroutine.running())
    skynet.timeout(500, task)
end
​
skynet.start(function ()  --skynet.start启动一个timeout来执行function,创建了一个协程
     skynet.error("start", coroutine.running())
    skynet.timeout(500, task) --由于function函数还没用完协程,所有这个timeout又创建了一个协程
end)

​ 执行结果,交替使用协程池中的协程:

testtimeout
[:0100000a] LAUNCH snlua testtimeout
[:0100000a] start thread: 0x7f525b16a048 false  #start函数也执行完,这个协程就空闲下来了
[:0100000a] task thread: 0x7f525b16a128 false   #当前服务的协程池中只有两个协程,所以是交替使用
[:0100000a] task thread: 0x7f525b16a048 false
[:0100000a] task thread: 0x7f525b16a128 false
[:0100000a] task thread: 0x7f525b16a048 false
 

6.7 获取时间

示例代码:testtime.lua

local skynet = require "skynet"function task()
    skynet.error("task")
    skynet.error("start time", skynet.starttime()) --获取skynet节点开始运行的时间
    skynet.sleep(200)
    skynet.error("time", skynet.time())     --获取当前时间
    skynet.error("now", skynet.now())       --获取skynet节点已经运行的时间
end
​
skynet.start(function ()
    skynet.fork(task)
end)
 

运行结果:

$ ./skynet examples/config
testtime
[:01000010] LAUNCH snlua testtime
[:01000010] task
[:01000010] start time 1517161846
[:01000010] time 1517161850.73
[:01000010] now 473

6.8 错误处理

​ lua中的错误处理都是通过assert以及error来抛异常,并且中断当前流程,skynet也不例外,但是你真的懂assert以及error吗?

​ 注意这里的error不是skynet.error,skynet.error单纯写日志的,并不会中断流程。

​ skynet中使用assert或error后,服务会不会中断,skynet节点会不会中断?

来看一个例子:

testassert.lua

local skynet = require "skynet"function task1()
    skynet.error("task1", coroutine.running())
    skynet.sleep(100)
    assert(nil)
    --error("error occurred")
    skynet.error("task2", coroutine.running(), "end")
end
​
function task2()
    skynet.error("task2", coroutine.running())
    skynet.sleep(500)
    skynet.error("task2", coroutine.running(), "end")
end
​
skynet.start(function ()  
    skynet.error("start", coroutine.running())
    skynet.fork(task1) 
    skynet.fork(task2) 
end)

运行结果:

testassert
[:0100000a] LAUNCH snlua testassert
[:0100000a] start thread: 0x7fd9b12938e8 false
[:0100000a] task1 thread: 0x7fd9b12939c8 false  
[:0100000a] task2 thread: 0x7fd9b1293aa8 false
[:0100000a] lua call [0 to :100000a : 2 msgsz = 0] error : ./lualib/skynet.lua:534: ./lualib/skynet.lua:156: ./my_workspace/testassert.lua:6: assertion failed!
stack traceback:
    [C]: in function 'assert'
    ./my_workspace/testassert.lua:6: in function 'task1'
    ./lualib/skynet.lua:468: in upvalue 'f'
    ./lualib/skynet.lua:106: in function <./lualib/skynet.lua:105>
stack traceback:
    [C]: in function 'assert'
    ./lualib/skynet.lua:534: in function 'skynet.dispatch_message'
[:0100000a] task2 thread: 0x7fd9b1293aa8 end


​ 上面的结果已经很明显了,开了两个协程分别执行task1、task2,task1断言后终止掉当前协程,不会再往下执行,但是task2还是能正常执行。skynet节点也没有挂掉,还是能正常运行。

​ 那么我们在处理skynet的错误的时候可以大胆的使用assert与error,并不需要关注错误。当然,一个好的服务端,肯定不能一直出现中断掉的协程。

如果不想把协程中断掉,可以使用pcall来捕捉异常,例如:

local skynet = require "skynet"function task1()
    skynet.error("task1", coroutine.running())
    skynet.sleep(100)
    assert(nil)
    skynet.error("task2", coroutine.running(), "end")
end
​
function task2()
    skynet.error("task2", coroutine.running())
    skynet.sleep(500)
    skynet.error("task2", coroutine.running(), "end")
end
​
skynet.start(function ()  
    skynet.error("start", coroutine.running())
    skynet.fork(pcall, task1) 
    skynet.fork(pcall, task2) 
end)

运行结果:

testassert
[:0100000a] LAUNCH snlua testassert
[:0100000a] start thread: 0x7f93d3967568 false
[:0100000a] task1 thread: 0x7f93d3967aa8 false
[:0100000a] task2 thread: 0x7f93d3967b88 false
[:0100000a] task2 thread: 0x7f93d3967b88 end
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值