Procesamiento de mensajes de capa lua del análisis de código fuente de skynet

El mecanismo de procesamiento de mensajes de la capa Lua está en lualib / skynet.lua, que proporciona la mayoría de las API de la capa Lua (que eventualmente llamarán las API de la capa c), incluido el procesamiento de la capa Lua al iniciar un servicio snlua, creando nuevos servicios, registrar acuerdos de servicio, cómo enviar mensajes, cómo tratar los mensajes enviados por la otra parte, etc. Este artículo presenta principalmente el mecanismo de procesamiento de mensajes para comprender cómo skynet logra una alta concurrencia.

Por simplicidad, coroutine_resume y coroutine_yield usados ​​en el código se pueden considerar como coroutine.resume y coroutine.yield.

local coroutine_resume = profile.resume
local coroutine_yield = profile.yield

1. Corutina

coroutine.create, crea un co, el único parámetro es el cierre f que debe ejecutar co, y el cierre f no se ejecutará en este momento

coroutine.resume, ejecuta un co, el primer parámetro es el manejador de co, si es la primera ejecución, otros parámetros se pasan al cierre f. Después de que se inicia co, continúa ejecutándose hasta que termina o cede. Terminación normal, devuelve verdadero y el valor de retorno del cierre f; si ocurre un error, se devuelve un mensaje de terminación anormal, falso y de error

coroutine.yield, para suspender co y renunciar al derecho de ejecución. El correspondiente al currículum más reciente regresará inmediatamente, devolviendo parámetros verdaderos y de rendimiento. La próxima vez que se reanude el mismo co, la ejecución continuará desde el punto de rendimiento. En este momento, la llamada de rendimiento volverá inmediatamente y el valor de retorno será reanudar parámetros distintos del primer parámetro.

Citando los documentos de Lua para presentar el ejemplo clásico de corrutina (denominado co), se puede ver que co se puede suspender y reiniciar continuamente. Skynet usa ampliamente co. Al enviar una solicitud rpc, suspenderá el co actual y lo reiniciará cuando la otra parte regrese.

 

2. Cómo Skynet crea una corrutina

Permítanme primero explicar cómo skynet crea una corrutina (co) y crea una corrutina a través de la api de co_create (f) Este código es muy interesante. Para el rendimiento, skynet pone el co creado en el caché (línea 9) Cuando la corrutina termina de ejecutar el proceso (cierre f), no terminará, pero se detendrá (línea 10). Cuando la persona que llama llama a la api co_create, si no está en la caché, crea un co a través de coroutine.create. En este momento, el cierre f no se ejecutará, y luego en un momento determinado (generalmente cuando se recibe un mensaje, se llama skynet.dispatch_message) Reiniciará (con los parámetros requeridos) este co, y luego ejecutará el cierre f (línea 6), y finalmente hará una pausa para esperar el siguiente uso, correspondiente al currículum más reciente return true y "EXIT "(línea 10); si es uno Reutilizar el co, reiniciar co (línea 15, el parámetro es el cierre f a ejecutar), rendimiento regresará inmediatamente y asignará el cierre af (línea 10), y pausará nuevamente en línea 11, y en algún momento reiniciará (con los parámetros requeridos) este co, luego co ejecuta el cierre f (línea 11), y finalmente se detiene en la línea 10 para el siguiente uso.

 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

Recomiende una explicación en video de Skynet: https://ke.qq.com/course/2806743?flowToken=1030833 , la explicación es detallada y hay materiales de documentación para el aprendizaje, los principiantes y los veteranos pueden verla.

3. Cómo lidiar con los mensajes de la capa Lua  

Después de comprender el principio de co_create, tomemos el servicio A enviando un mensaje al servicio B como ejemplo para ilustrar cómo skynet procesa los mensajes de la capa 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)

 Al final del inicio del servicio, se llamará skynet.start, skynet.start llamará skynet.timeout y se creará una co (línea 12) en el tiempo de espera, que se denomina co1 de la co-rutina principal del servicio. esta vez, co1 no se ejecutará.

 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

Cuando se activa el temporizador (debido a que el temporizador está configurado en 0, se activará el siguiente marco) enviará un mensaje de tipo "RESPUESTA" (PTYPE_RESPONSE = 1) al servicio

// skynet-src/skynet_timer.c
static inline void
dispatch_list(struct timer_node *current) {
    ...
    message.sz = (size_t)PTYPE_RESPONSE << MESSAGE_TYPE_SHIFT;
    ...
}

 Una vez que el servicio recibe el mensaje, llama a la API de distribución de mensajes. Dado que el tipo de mensaje es RESPONSE, eventualmente se ejecutará en la línea 7. Reinicie la corrutina principal co1 y ejecute el cierre f de co1 (aquí está skynet.init_service (start_func)). Si no hay una operación suspendida en el cierre f, después de que el cierre f se haya ejecutado con éxito, co1 se suspende y reanudar volverá true y "EXIT", a continuación, la línea 7 se convierte en, 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

Luego, llame a suspender, porque el tipo es "EXIT", simplemente haga un trabajo de limpieza.

-- 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

Cuando hay una operación de pausa en el cierre f, por ejemplo, el servicio A envía el mensaje skynet.call ("B", "lua", "aaa") al servicio B, aquí se explica cómo manejar el servicio A y el servicio B:

Para el servicio A:

Primero envíe el mensaje en la capa c (línea 14, envíe el mensaje a la cola de mensajes secundaria del servicio de destino), luego pause co1, resume devuelve verdadero, "CALL" y valor de sesión

 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

 Luego llame a suspender (co, true, "CALL", session), el tipo es "CALL", session es la clave, co es el valor y se almacena en session_id_coroutine, de modo que cuando el servicio B regrese de la solicitud de A, Puede encontrar el correspondiente según la sesión co, para que pueda reiniciar 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

Cuando A recibe el mensaje de retorno de B, llama a la api de distribución de mensajes, encuentra el co correspondiente (es decir, la co-rutina principal co1) de acuerdo con la sesión y lo reinicia desde el último punto de pausa. La siguiente línea de código yield return inmediatamente e imprime el retorno de B El resultado de print (...) (A.lua), cuando se ejecuta todo el proceso de co1, devuelve verdadero y "EXIT" para suspender, y realiza un trabajo de limpieza en co1.

local succ, msg, sz = coroutine_yield("CALL", session)

Cambie A.lua un poco. En el proceso de co1 ejecutando el cierre f, se crea una corrutina (llamada co2) por fork. Dado que co1 no está suspendido, siempre se ejecutará todo el proceso. En este momento, el co2 no se ejecuta. 

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

Lo segundo que hace la API de distribución de mensajes es procesar el co en fork_queue. Entonces, lo segundo que debe hacer después de recibir el mensaje enviado de vuelta por el temporizador es reiniciar el co2 y luego pausar el co2 después de enviar un mensaje al servicio B, y luego reiniciar el co2 nuevamente cuando B regrese.

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

Para el servicio B:

 Después de recibir el mensaje del servicio A, llame a la API de distribución de mensajes para crear un co (línea 12), el cierre f que ejecutará co es la función de devolución de llamada de mensaje registrado p.dispatch (línea 4), y luego reinícielo a través de resume (Línea 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

Ejecute skynet.ret (skynet.pack ("OK")), llame a yield para suspenderlo (línea 4), el currículum más reciente regresa, la línea 15 anterior se convierte en 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

 Cuando el comando == "RETURN", haga dos cosas: 1. Envíe un mensaje de respuesta a la dirección de origen (es decir, un servicio) (línea 5); 2. Reinicie co (línea 7), y co regresa de skynet.ret, luego se ejecuta la función de devolución de llamada de mensaje (p.dispatch) del servicio B, y todo el cierre f de co se ejecuta y se coloca en la caché, devolviendo verdadero y "EXIT" para suspender

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

Hasta ahora, es todo el proceso de procesamiento de mensajes de la capa Lua.

4. Manejo de excepciones

En algunos casos, se requiere el manejo de excepciones, como no registrar el protocolo correspondiente al tipo de mensaje, no proporcionar una función de devolución de llamada de mensaje y se produjo un error durante la ejecución de co. Cuando ocurre una excepción en el proceso de un servicio que procesa un mensaje, se deben hacer dos cosas: 1. Terminar de manera anormal la comunicación actual 2. Notificar al remitente del mensaje en lugar de mantener a la otra parte ocupada esperando.

Cuando ocurre un error durante la ejecución de co, el primer valor devuelto de resume es falso, se llama a suspend y se envía un mensaje de tipo PTYPE_ERROR a la otra parte (línea 9), y luego se lanza una excepción para terminar el co actual. (línea 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

En la mayoría de las situaciones anormales, se enviará un mensaje de tipo PTYPE_ERROR a la otra parte para notificar a la otra parte. Cuando se reciba un mensaje de tipo PYTPE_ERROR, se llamará _error_dispatch, error_source se registrará en dead_service y error_session se registrará en 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

Al final de la suspensión, se llama dispatch_error_queue para procesar error_queue, el co en espera se encuentra a través de la sesión y luego se termina a la fuerza para garantizar que el co no estará ocupado esperando todo el tiempo.

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. Resumen

El flujo de una solicitud rpc sincronizada es el siguiente. Cuando se suspende el co actual de un servicio, se pueden ejecutar los procesos de otros cos en el servicio. N cos se puede ejecutar de forma cruzada. La suspensión de un co no afectará la ejecución de otros cos, maximizando la provisión de potencia de cómputo y lograr una alta concurrencia.

 

 

Supongo que te gusta

Origin blog.csdn.net/Linuxhus/article/details/111669559
Recomendado
Clasificación