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