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

 

需要注意的是:skynet.wakeup除了能喚醒wait線程,也可以喚醒sleep的線程

 

 

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

 

發佈了101 篇原創文章 · 獲贊 117 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章