Le mécanisme de traitement des messages de la couche Lua se trouve dans lualib / skynet.lua, qui fournit la plupart des API de la couche Lua (qui appelleront éventuellement les API de la couche c), y compris le traitement de la couche Lua lors du démarrage d'un service snlua, la création de nouveaux services, enregistrement des contrats de service, comment envoyer des messages, comment traiter les messages envoyés par l'autre partie, etc. Cet article présente principalement le mécanisme de traitement des messages pour comprendre comment skynet atteint une concurrence élevée.
Par souci de simplicité, coroutine_resume et coroutine_yield utilisés dans le code peuvent être considérés comme coroutine.resume et coroutine.yield.
local coroutine_resume = profile.resume
local coroutine_yield = profile.yield
1. Coroutine
coroutine.create, crée un co, le seul paramètre est la fermeture f à exécuter par co, et la fermeture f ne sera pas exécutée à ce moment
coroutine.resume, exécute un co, le premier paramètre est le handle de co, si c'est la première exécution, d'autres paramètres sont passés à la fermeture f. Une fois co démarré, il continue de s'exécuter jusqu'à ce qu'il se termine ou cède. Terminaison normale, retourne vrai et la valeur de retour de fermeture f; si une erreur se produit, un arrêt anormal, un message faux et d'erreur sont renvoyés
coroutine.yield, de suspendre co et de renoncer au droit d'exécution. Correspondant à la reprise la plus récente retournera immédiatement, renvoyant les paramètres true et yield. La prochaine fois que le même co est repris, l'exécution se poursuivra à partir du point de rendement. À ce stade, l'appel de rendement sera renvoyé immédiatement et la valeur renvoyée sera des paramètres de reprise autres que le premier paramètre.
Citant des documents Lua pour présenter l'exemple classique de coroutine (appelé co), on peut voir que co peut être continuellement suspendu et redémarré. Skynet utilise largement co. Lors de l'envoi d'une requête rpc, il suspendra le co actuel et le redémarrera lorsque l'autre partie reviendra.
2. Comment skynet crée une coroutine
Permettez-moi d'abord d'expliquer comment skynet crée une coroutine (co), et crée une coroutine via l'API de co_create (f). Ce code est très intéressant. Pour les performances, skynet met le co créé dans le cache (ligne 9) .Lorsque la coroutine a fini d'exécuter le processus (fermeture f), il ne se terminera pas, mais se mettra en pause (ligne 10). Lorsque l'appelant appelle l'api co_create, s'il n'est pas dans le cache, créez un co via coroutine.create. A ce moment, la fermeture f ne sera pas exécutée, puis à un certain moment (généralement lorsqu'un message est reçu, le skynet.dispatch_message est appelé) Redémarrera (avec les paramètres requis) ce co, puis exécutera la fermeture f (ligne 6), et enfin s'arrêtera pour attendre la prochaine utilisation, correspondant à la reprise la plus récente retournera true et "EXIT "(ligne 10); s'il en est un Réutilisez le co, redémarrez co (ligne 15, le paramètre est la fermeture f à exécuter), yield retournera immédiatement et affectera la fermeture à f (ligne 10), et s'arrêtera à nouveau à ligne 11, et à un moment donné, il redémarrera (avec les paramètres requis) ce co, puis co exécutera la fermeture f (ligne 11), et enfin s'arrêtera sur la ligne 10 pour la prochaine utilisation.
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
Recommandez une explication vidéo Skynet: https://ke.qq.com/course/2806743?flowToken=1030833 , l'explication est détaillée et il existe du matériel de documentation pour l'apprentissage, les novices et les vétérans peuvent le voir.
3. Comment gérer les messages de la couche Lua
Après avoir compris le principe de co_create, prenons le service A envoyant un message au service B comme exemple pour illustrer comment skynet traite les messages de la couche Lua:
-- 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)
A la fin du démarrage du service, skynet.start sera appelé, skynet.start appellera skynet.timeout, et un co (ligne 12) sera créé dans le timeout, qui est appelé coroutine principale co1 du service. cette fois, co1 ne sera pas exécuté.
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
Lorsque le timer est déclenché (parce que le timer est mis à 0, la trame suivante sera déclenchée) enverra un message de type "RESPONSE" (PTYPE_RESPONSE = 1) au service
// skynet-src/skynet_timer.c
static inline void
dispatch_list(struct timer_node *current) {
...
message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
...
}
Une fois que le service a reçu le message, il appelle l'API de distribution de messages. Puisque le type de message est RESPONSE, il s'exécutera finalement à la ligne 7. Redémarrez la coroutine principale co1 et exécutez la fermeture f de co1 (voici skynet.init_service (start_func)). S'il n'y a pas d'opération suspendue dans la fermeture f, une fois la fermeture f exécutée avec succès, co1 est suspendu et la reprise reviendra true et "EXIT", ensuite, la ligne 7 devient, 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
Ensuite, appelez suspend, car le type est "EXIT", faites simplement un travail de nettoyage.
-- 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
Lorsqu'il y a une opération de pause à la fermeture f, par exemple, le service A envoie le message skynet.call ("B", "lua", "aaa") au service B, voici comment gérer le service A et le service B:
Pour le service A:
Envoyez d'abord le message dans la couche c (ligne 14, poussez le message dans la file d'attente de messages secondaire du service de destination), puis mettez en pause co1, reprendre renvoie true, "CALL" et la valeur de session
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
Puis appelez suspend (co, true, "CALL", session), le type est "CALL", session est la clé, co est la valeur et stockée dans session_id_coroutine, de sorte que lorsque le service B revient de la requête de A, il pouvez trouver le correspondant en fonction de la session co, vous pouvez donc redémarrer 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
Lorsque A reçoit le message de retour de B, il appelle l'API de distribution de messages, trouve le co correspondant (c'est-à-dire la coroutine principale co1) en fonction de la session et le redémarre à partir du dernier point de pause. La ligne de code suivante sera retourne immédiatement et affiche le retour de B Le résultat de print (...) (A.lua), lorsque tout le processus de co1 est exécuté, retourne true et "EXIT" pour suspendre, et faire un travail de nettoyage sur co1.
local succ, msg, sz = coroutine_yield("CALL", session)
Changez un peu A.lua. Dans le processus d'exécution de la fermeture f de co1, une coroutine (appelée co2) est créée par fork. Puisque co1 n'est pas suspendu, tout le processus sera toujours exécuté. A ce moment, co2 n'est pas exécuté.
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
La deuxième chose que fait l'API de distribution de messages est de traiter le co dans fork_queue. Ainsi, la deuxième chose à faire après avoir reçu le message renvoyé par le minuteur est de redémarrer co2, puis de mettre en pause co2 après avoir envoyé un message au service B, puis de redémarrer à nouveau co2 lorsque B revient.
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
Pour le service B:
Après avoir reçu le message du service A, appelez l'API de distribution de messages pour créer un co (ligne 12), la fermeture f à exécuter par co est la fonction de rappel de message enregistré p.dispatch (ligne 4), puis redémarrez-la par reprise (Ligne 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
Exécutez skynet.ret (skynet.pack ("OK")), appelez yield pour le suspendre (ligne 4), le retour le plus récent revient, la ligne 15 ci-dessus devient 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
Lorsque la commande == "RETURN", faites deux choses: 1. Envoyez un message de retour à l'adresse source (c'est-à-dire un service) (ligne 5); 2. Redémarrez co (ligne 7), et co revient de skynet.ret, puis la fonction de rappel de message (p.dispatch) du service B est exécutée, et toute la fermeture f de co est exécutée et mise dans le cache, retournant true et "EXIT" pour suspendre
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
Jusqu'à présent, il s'agit de l'ensemble du processus de traitement des messages de la couche Lua.
4. Traitement des exceptions
Dans certains cas, la gestion des exceptions est nécessaire, comme ne pas enregistrer le protocole correspondant au type de message, ne pas fournir de fonction de rappel de message et une erreur s'est produite lors de l'exécution de co. Lorsqu'une exception se produit dans le processus d'un service traitant un message, deux choses doivent être faites: 1. Mettre fin anormalement au co actuel 2. Aviser l'expéditeur du message au lieu de garder l'autre partie occupée en attente.
Lorsqu'une erreur survient lors de l'exécution de co, la première valeur de retour de resume est false, suspend est appelé, et un message de type PTYPE_ERROR est envoyé à l'autre partie (ligne 9), puis une exception est levée pour terminer le co en cours (ligne 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
Dans la plupart des situations anormales, un message de type PTYPE_ERROR sera envoyé à l'autre partie pour avertir l'autre partie. Lorsqu'un message de type PYTPE_ERROR est reçu, _error_dispatch sera appelé, error_source sera enregistrée dans dead_service et error_session sera enregistrée dans 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
À la fin de la suspension, dispatch_error_queue est appelé pour traiter error_queue, le co en attente est trouvé à travers la session, puis il est interrompu de force pour s'assurer que le co ne sera pas occupé à attendre tout le temps.
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. Résumé
Le flux d'une requête rpc synchronisée est le suivant. Lorsque le co actuel d'un service est suspendu, les processus des autres cos du service peuvent être exécutés. N cos peut être exécuté de manière croisée. La suspension d'un co n'affectera pas l'exécution de l'autre cos, maximisant la fourniture de puissance de calcul et obtenir une concurrence élevée.