Luaレイヤーメッセージ処理メカニズムはlualib / skynet.luaにあり、snluaサービスの開始時のLuaレイヤーの処理、新しいサービスの作成など、ほとんどのLuaレイヤーAPI(最終的にはcレイヤーAPIと呼ばれます)を提供します。サービス契約の登録、メッセージの送信方法、相手から送信されたメッセージの処理方法など。この記事では主に、skynetがどのように高い同時実行性を実現するかを理解するためのメッセージ処理メカニズムを紹介します。
簡単にするために、コードで使用されているcoroutine_resumeとcoroutine_yieldは、coroutine.resumeとcoroutine.yieldと見なすことができます。
local coroutine_resume = profile.resume
local coroutine_yield = profile.yield
1.コルーチン
coroutine.create、coを作成します。唯一のパラメーターは、coによって実行されるクロージャーfであり、クロージャーfはこの時点では実行されません。
coroutine.resume、coを実行します。最初のパラメーターはcoのハンドルです。これが最初の実行である場合、他のパラメーターはクロージャーfに渡されます。coが開始された後、終了または譲歩するまで実行を続けます。正常終了、trueを返し、クロージャfの戻り値。エラーが発生した場合、異常終了、false、およびエラーメッセージが返されます。
coroutine.yield、coを一時停止し、実行権を放棄します。最新の履歴書に対応すると、すぐに戻り、trueおよびyieldパラメーターが返されます。次回同じcoが再開されると、yieldのポイントから実行が続行されます。このとき、yield呼び出しはすぐに戻り、戻り値は最初のパラメーター以外の再開パラメーターになります。
コルーチンの古典的な例(coと呼ばれる)を紹介するためにLuaのドキュメントを引用すると、coを継続的に一時停止および再開できることがわかります。Skynetはcoを広く使用しています。rpcリクエストを送信すると、現在のcoを一時停止し、相手が戻ったときに再開します。
2.スカイネットがコルーチンを作成する方法
まず、skynetがコルーチン(co)を作成する方法を説明し、co_create(f)のAPIを使用してコルーチンを作成します。このコードは非常に興味深いものです。パフォーマンスのために、skynetは作成されたcoをキャッシュに入れます(9行目)。コルーチンがプロセスの実行を終了すると(クロージャf)、コルーチンは終了しませんが、一時停止します(10行目)。呼び出し元がco_createapiを呼び出すときに、それがキャッシュにない場合は、coroutine.createを介してcoを作成します。この時点では、クロージャーfは実行されず、特定の時点(通常はメッセージが受信されたとき)に実行されます。 skynet.dispatch_messageが呼び出されます)このcoを(必要なパラメーターで)再起動し、クロージャーf(6行目)を実行し、最後に一時停止して次の使用を待ちます。これは、最新の再開に対応してtrueを返し、「EXIT "(10行目);それが1つの場合、coを再利用し、coを再起動し(15行目、パラメーターは実行されるクロージャーfです)、yieldはすぐに戻り、クロージャーをfに割り当てます(10行目)。 11行目、ある時点でこのcoを(必要なパラメーターを使用して)再起動し、次にcoがクロージャーf(11行目)を実行し、最後に次の使用のために10行目で一時停止します。
1 -- lualib/skynet.lua
2 local function co_create(f)
3 local co = table.remove(coroutine_pool)
4 if co == nil then
5 co = coroutine.create(function(...)
6 f(...)
7 while true do
8 f = nil
9 coroutine_pool[#coroutine_pool+1] = co
10 f = coroutine_yield "EXIT"
11 f(coroutine_yield())
12 end
13 end)
14 else
15 coroutine_resume(co, f)
16 end
17 return co
18 end
Skynetビデオの説明をお勧めします:https://ke.qq.com/course/2806743?flowToken = 1030833、説明は詳細であり、学習用のドキュメント資料があり、初心者やベテランはそれを見ることができます。
3.Luaレイヤーメッセージの処理方法
co_createの原則を理解した後、スカイネットがLuaレイヤーメッセージを処理する方法を説明する例として、サービスAがサービスBにメッセージを送信する方法を見てみましょう。
-- A.lua
local skynet = require "skynet"
skynet.start(function()
print(skynet.call("B", "lua", "aaa"))
end)
-- B.lua
local skynet = require "skynet"
require "skynet.manager"
skynet.start(function()
skynet.dispatch("lua", function(session, source, ...)
skynet.ret(skynet.pack("OK"))
end)
skynet.register "B"
end)
サービス開始の最後に、skynet.startが呼び出され、skynet.startがskynet.timeoutを呼び出し、タイムアウトにco(12行目)が作成されます。これは、サービスのメインコルーチンco1と呼ばれます。今回はco1は実行されません。
1 -- lualib/skynet.lua
2 function skynet.start(start_func)
3 c.callback(skynet.dispatch_message)
4 skynet.timeout(0, function()
5 skynet.init_service(start_func)
6 end)
7 end
8
9 function skynet.timeout(ti, func)
10 local session = c.intcommand("TIMEOUT",ti)
11 assert(session)
12 local co = co_create(func)
13 assert(session_id_coroutine[session] == nil)
14 session_id_coroutine[session] = co
15 end
タイマーがトリガーされると(タイマーが0に設定されているため、次のフレームがトリガーされます)、「RESPONSE」タイプ(PTYPE_RESPONSE = 1)メッセージがサービスに送信されます。
// skynet-src/skynet_timer.c
static inline void
dispatch_list(struct timer_node *current) {
...
message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
...
}
サービスはメッセージを受信した後、メッセージ配信APIを呼び出します。メッセージタイプはRESPONSEであるため、最終的には7行目まで実行されます。メインコルーチンco1を再起動し、co1のクロージャーfを実行します(ここではskynet.init_service(start_func))。クロージャーfに中断された操作がない場合、クロージャーfが正常に実行された後、co1は中断され、再開が返されます。 trueおよび "EXIT"、次に、7行目は、suspend(co、true、 "EXIT")になります。
1 -- luablib/skynet.lua
2 local function raw_dispatch_message(prototype, msg, sz, session, source)
3 -- skynet.PTYPE_RESPONSE = 1, read skynet.h
4 if prototype == 1 then
5 local co = session_id_coroutine[session]
6 ...
7 suspend(co, coroutine_resume(co, true, msg, sz))
8 ...
9 end
次に、タイプが「EXIT」であるため、suspendを呼び出します。クリーンアップ作業を実行するだけです。
-- lualib/skynet.lua
function suspend(co, result, command, param, size)
...
elseif command == "EXIT" then
-- coroutine exit
local address = session_coroutine_address[co]
if address then
release_watching(address)
session_coroutine_id[co] = nil
session_coroutine_address[co] = nil
session_response[co] = nil
end
...
end
たとえば、クロージャfに一時停止操作がある場合、サービスAはメッセージskynet.call( "B"、 "lua"、 "aaa")をサービスBに送信します。サービスAとサービスBの処理方法は次のとおりです。
サービスAの場合:
最初にcレイヤーでメッセージを送信し(14行目、メッセージを宛先サービスのセカンダリメッセージキューにプッシュします)、次にco1を一時停止し、resumeがtrue、「CALL」、およびセッション値を返します。
1 -- lualib/skynet.lua
2 local function yield_call(service, session)
3 watching_session[session] = service
4 local succ, msg, sz = coroutine_yield("CALL", session)
5 watching_session[session] = nil
6 if not succ then
7 error "call failed"
8 end
9 return msg,sz
10 end
11
12 function skynet.call(addr, typename, ...)
13 local p = proto[typename]
14 local session = c.send(addr, p.id , nil , p.pack(...))
15 if session == nil then
16 error("call to invalid address " .. skynet.address(addr))
17 end
18 return p.unpack(yield_call(addr, session))
19 end
次に、suspend(co、true、 "CALL"、session)を呼び出します。タイプは「CALL」、セッションはキー、coは値であり、session_id_coroutineに格納されるため、BサービスがAの要求から戻ると、セッションcoに従って対応するものを見つけることができるので、coを再起動できます
1 -- lualib/skynet.lua
2 function suspend(co, result, command, param, size)
3 ...
4 if command == "CALL" then
5 session_id_coroutine[param] = co
6 ...
7 end
AはBからリターンメッセージを受信すると、メッセージ配信apiを呼び出し、セッションに従って対応するco(つまり、メインコルーチンco1)を見つけ、最後の一時停止ポイントから再開します。次のコード行は次のようになります。すぐに戻り、Bの戻り値を出力します。print(...)(A.lua)の結果、co1のプロセス全体が実行されると、trueと「EXIT」を返して一時停止し、co1のクリーンアップ作業を行います。
local succ, msg, sz = coroutine_yield("CALL", session)
A.luaを少し変更します。co1がクロージャーfを実行するプロセスでは、コルーチン(co2と呼ばれる)がforkによって作成されます。co1は中断されないため、プロセス全体が常に実行されます。現時点では、co2は実行されていません。
1 -- A.lua
2 local skynet = require "skynet"
3
4 skynet.start(function()
5 skynet.fork(function()
6 print(skynet.call("B", "lua", "aaa"))
7 end)
8 end)
1 -- lualib/skynet.lua
2 function skynet.fork(func,...)
3 local args = table.pack(...)
4 local co = co_create(function()
5 func(table.unpack(args,1,args.n))
6 end)
7 table.insert(fork_queue, co)
8 return co
9 end
メッセージ配信APIが行う2番目のことは、fork_queueでcoを処理することです。したがって、タイマーによって返送されたメッセージを受信した後に行う2番目のことは、co2を再起動し、メッセージをBサービスに送信した後にco2を一時停止し、Bが戻ったときにco2を再起動することです。
1 -- lualib/skynet.lua
2 function skynet.dispatch_message(...)
3 ...
4 local fork_succ, fork_err = pcall(suspend,co,coroutine_resume(co))
5 ...
6 end
サービスBの場合:
サービスAのメッセージを受信した後、メッセージ配信APIを呼び出してcoを作成し(12行目)、coによって実行されるクロージャーfは登録済みメッセージコールバック関数p.dispatch(4行目)であり、再開して再開します(15行目)
1 -- lualib/skynet.lua
2 local function raw_dispatch_message(prototype, msg, sz, session, source)
3 ...
4 local f = p.dispatch
5 if f then
6 local ref = watching_service[source]
7 if ref then
8 watching_service[source] = ref + 1
9 else
10 watching_service[source] = 1
11 end
12 local co = co_create(f)
13 session_coroutine_id[co] = session
14 session_coroutine_address[co] = source
15 suspend(co, coroutine_resume(co, session,source, p.unpack(msg,sz)))
16 ...
17 end
skynet.ret(skynet.pack( "OK"))を実行し、yieldを呼び出して一時停止します(4行目)。最新の履歴書が返され、上記の15行目がsuspend(co、true、 "RETURN"、msg、sz)になります。
1 -- lualib/skynet.lua
2 function skynet.ret(msg, sz)
3 msg = msg or ""
4 return coroutine_yield("RETURN", msg, sz)
5 end
command == "RETURN"の場合、次の2つのことを行います。1。送信元アドレス(つまりAサービス)にリターンメッセージを送信します(5行目)。2。coを再起動し(7行目)、coはskynet.retから戻ります。 Bサービスのメッセージコールバック関数(p.dispatch)が実行され、coのすべてのクロージャーfが実行されてキャッシュに入れられ、trueと「EXIT」を返して一時停止します。
1 -- lualib/skynet.lua
2 function suspend(co, result, command, param, size)
3 ...
4 elseif command == "RETURN" then
5 ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, param, size) ~= nil
6 ...
7 return suspend(co, coroutine_resume(co, ret))
8 ...
9 end
これまでのところ、これはLuaレイヤーのメッセージ処理のプロセス全体です。
4.例外処理
メッセージタイプに対応するプロトコルを登録しない、メッセージコールバック関数を提供しない、coの実行中にエラーが発生するなど、例外処理が必要な場合があります。メッセージを処理するサービスのプロセスで例外が発生した場合、次の2つのことを行う必要があります。1。現在のcoを異常終了します。2。相手をビジー待機させるのではなく、メッセージの送信者に通知します。
coの実行中にエラーが発生すると、resumeの最初の戻り値がfalseになり、suspendが呼び出され、PTYPE_ERRORタイプのメッセージが相手に送信され(9行目)、例外がスローされて現在のcoが終了します。 (14行目)。
1 -- lualib/skynet.lua
2 function suspend(co, result, command, param, size)
3 if not result then
4 local session = session_coroutine_id[co]
5 if session then -- coroutine may fork by others (session is nil)
6 local addr = session_coroutine_address[co]
7 if session ~= 0 then
8 -- only call response error
9 c.send(addr, skynet.PTYPE_ERROR, session, "")
10 end
11 session_coroutine_id[co] = nil
12 session_coroutine_address[co] = nil
13 end
14 error(debug.traceback(co,tostring(command)))
15 end
16 ...
17 end
ほとんどの異常な状況では、PTYPE_ERRORタイプのメッセージが相手に送信され、相手に通知されます。PYTPE_ERRORタイプのメッセージが受信されると、_error_dispatchが呼び出され、error_sourceがdead_serviceに記録され、error_sessionがerror_queueに記録されます。
1 -- lualib/skynet.lua
2 local function _error_dispatch(error_session, error_source)
3 if error_session == 0 then
4 -- service is down
5 -- Don't remove from watching_service , because user may call dead service
6 if watching_service[error_source] then
7 dead_service[error_source] = true
8 end
9 for session, srv in pairs(watching_session) do
10 if srv == error_source then
11 table.insert(error_queue, session)
12 end
13 end
14 else
15 -- capture an error for error_session
16 if watching_session[error_session] then
17 table.insert(error_queue, error_session)
18 end
19 end
20 end
サスペンドの終了時に、dispatch_error_queueが呼び出されてerror_queueが処理され、セッションを通じて待機中のcoが検出されます。その後、coが常にビジー状態にならないように、強制的に終了されます。
1 -- lualib/skynet.lua
2 local function dispatch_error_queue()
3 local session = table.remove(error_queue,1)
4 if session then
5 local co = session_id_coroutine[session]
6 session_id_coroutine[session] = nil
7 return suspend(co, coroutine_resume(co, false))
8 end
9 end
5.まとめ
同期されたrpcリクエストのフローは次のとおりです。サービスの現在のcoが一時停止されると、サービス内の他のcosのプロセスを実行できます。Ncosは相互実行できます。1つのcoの一時停止は、他のcosの実行に影響を与えず、計算能力の提供を最大化します。高い同時実行性を実現します。