Processamento de mensagens da camada Lua da análise do código-fonte da skynet

O mecanismo de processamento de mensagens da camada Lua está em lualib / skynet.lua, que fornece a maioria das APIs da camada Lua (que eventualmente chamarão as APIs da camada C), incluindo o processamento da camada Lua ao iniciar um serviço snlua, criando novos serviços, registrar contratos de serviço, como enviar mensagens, como lidar com as mensagens enviadas pela outra parte, etc. Este artigo apresenta principalmente o mecanismo de processamento de mensagens para entender como a skynet atinge alta simultaneidade.

Para simplificar, o coroutine_resume e coroutine_yield usados ​​no código podem ser considerados como coroutine.resume e coroutine.yield.

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

1. Corrotina

coroutine.create, crie um co, o único parâmetro é o fecho f a ser executado por co, e o fecho f não será executado neste momento

coroutine.resume, execute um co, o primeiro parâmetro é o manipulador de co, se for a primeira execução, outros parâmetros são passados ​​para o encerramento f. Depois que co é iniciado, ele continua a executar até terminar ou render. Encerramento normal, retorna verdadeiro e o valor de retorno de encerramento f; se ocorrer um erro, encerramento anormal, falso e mensagem de erro são retornados

coroutine.yield, para suspender co e renunciar ao direito de execução. O currículo correspondente ao mais recente retornará imediatamente, retornando os parâmetros true e yield. Na próxima vez que o mesmo co for retomado, a execução continuará a partir do ponto de rendimento. Neste momento, a chamada de rendimento retornará imediatamente, e o valor de retorno será parâmetros de retomada diferentes do primeiro parâmetro

Citando documentos Lua para apresentar o exemplo clássico de co-rotina (referido como co), pode-se ver que co pode ser continuamente suspenso e reiniciado. A Skynet usa amplamente o co. Ao enviar uma solicitação de rpc, ele suspende o co atual e o reinicia quando a outra parte retorna.

 

2. Como a skynet cria uma co-rotina

Deixe-me primeiro explicar como a skynet cria uma corrotina (co) e cria uma corrotina por meio da api de co_create (f). Este código é muito interessante. Para desempenho, a skynet coloca o co criado no cache (linha 9) .Quando a co-rotina termina de executar o processo (encerramento f), ela não terminará, mas fará uma pausa (linha 10). Quando o chamador chama a API co_create, se ela não estiver no cache, crie um co através de coroutine.create. Neste momento, o encerramento f não será executado, e então em um determinado momento (geralmente quando uma mensagem é recebida, a skynet.dispatch_message é chamada) irá reiniciar (com os parâmetros necessários) este co, e então executar o encerramento f (linha 6) e, finalmente, fazer uma pausa para aguardar o próximo uso, correspondendo ao retorno de currículo mais recente verdadeiro e "SAIR "(linha 10); se for um Reutilize o co, reinicie co (linha 15, o parâmetro é o fechamento f a ser executado), o rendimento retornará imediatamente e atribuirá o fechamento a f (linha 10), e pausará novamente em linha 11, e em algum ponto ele irá reiniciar (com os parâmetros necessários) este co, então co executa o fechamento f (linha 11) e, finalmente, faz uma pausa na linha 10 para o próximo 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

Recomendar uma explicação em vídeo da Skynet: https://ke.qq.com/course/2806743?flowToken=1030833 , a explicação é detalhada e há materiais de documentação para aprendizagem, novatos e veteranos podem vê-la.

3. Como lidar com mensagens da camada Lua  

Depois de entender o princípio de co_create, vamos pegar o serviço A enviando uma mensagem para o serviço B como um exemplo para ilustrar como a skynet processa as mensagens da camada 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)

 No final do início do serviço, skynet.start será chamado, skynet.start chamará skynet.timeout e um co (linha 12) será criado no tempo limite, que é chamado de co-rotina principal co1 do serviço. desta vez, co1 não será executado.

 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

Quando o cronômetro é acionado (porque o cronômetro está definido como 0, o próximo quadro será acionado) enviará uma mensagem do tipo "RESPOSTA" (PTYPE_RESPONSE = 1) para o serviço

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

 Depois que o serviço recebe a mensagem, ele chama a API de distribuição de mensagens. Como o tipo de mensagem é RESPONSE, ele será executado na linha 7. Reinicie a co-rotina principal co1 e execute o fechamento f de co1 (aqui está skynet.init_service (start_func)). Se não houver operação suspensa no fechamento f, após o fechamento f ser executado com sucesso, co1 é suspensa e a retomada retornará true e "EXIT", a seguir, a linha 7 torna-se, 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

Em seguida, chame a suspensão, pois o tipo é "SAIR", basta fazer um trabalho de limpeza.

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

Quando há uma operação de pausa no fechamento f, por exemplo, o serviço A envia a mensagem skynet.call ("B", "lua", "aaa") para o serviço B, aqui estão como tratar o serviço A e o serviço B:

Para o serviço A:

Primeiro, envie a mensagem na camada c (linha 14, envie a mensagem para a fila de mensagens secundária do serviço de destino), em seguida, pause co1, retoma retorna verdadeiro, "CALL" e o valor da sessão

 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

 Em seguida, chame a suspensão (co, verdadeiro, "CALL", sessão), o tipo é "CALL", a sessão é a chave, co é o valor e armazenado em session_id_coroutine, para que quando o serviço B retornar da solicitação de A, ele pode encontrar o correspondente de acordo com a sessão co, então você pode 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

Quando A recebe a mensagem de retorno de B, ele chama a API de distribuição de mensagem, encontra o co correspondente (ou seja, a co-rotina principal co1) de acordo com a sessão e a reinicia a partir do último ponto de pausa. A linha de código a seguir produzirá retorne imediatamente e imprima o retorno de B O resultado de print (...) (A.lua), quando todo o processo de co1 for executado, retorne true e "EXIT" para suspender, e faça algum trabalho de limpeza em co1.

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

Altere um pouco A.lua. No processo de co1 executando o fechamento f, uma co-rotina (chamada co2) é criada por fork.Uma vez que co1 não está suspensa, todo o processo sempre será executado. Neste momento, o co2 não é executado. 

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

A segunda coisa que a API de distribuição de mensagens faz é processar o co em fork_queue. Portanto, a segunda coisa a fazer após receber a mensagem enviada de volta pelo cronômetro é reiniciar o co2 e, em seguida, pausar o co2 após enviar uma mensagem para o serviço B e, em seguida, reiniciar o co2 novamente quando B retornar.

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 o serviço B:

 Depois de receber a mensagem do serviço A, chame a api de distribuição de mensagem para criar um co (linha 12), o encerramento f a ser executado por co é a função de retorno de chamada de mensagem registrada p.dispatch (linha 4) e reinicie-a por meio de retomar (Linha 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

Execute skynet.ret (skynet.pack ("OK")), chame yield para suspendê-lo (linha 4), o currículo mais recente retorna, a linha 15 acima torna-se suspensa (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

 Quando o comando == "RETURN", faça duas coisas: 1. Envie uma mensagem de retorno para o endereço de origem (ou seja, um serviço) (linha 5); 2. Reinicie co (linha 7) e co retorne de skynet.ret, então a função de retorno de chamada da mensagem (p.dispatch) do serviço B é executada, e todo o fechamento f de co é executado e colocado no cache, retornando verdadeiro e "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

Até agora, é todo o processo de processamento de mensagens da camada Lua.

4. Tratamento de exceções

Em alguns casos, o tratamento de exceções é necessário, como não registrar o protocolo correspondente ao tipo de mensagem, não fornecer uma função de retorno de chamada de mensagem e ocorrer um erro durante a execução de co. Quando ocorre uma exceção no processo de processamento de uma mensagem de um serviço, duas coisas devem ser feitas: 1. Encerrar anormalmente o co atual; 2. Notificar o remetente da mensagem em vez de manter a outra parte ocupada esperando.

Quando ocorre um erro durante a execução de co, o primeiro valor de retorno de currículo é falso, suspende é chamado e uma mensagem do tipo PTYPE_ERROR é enviada para a outra parte (linha 9) e, em seguida, uma exceção é lançada para encerrar o co atual (linha 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

Na maioria das situações anormais, uma mensagem do tipo PTYPE_ERROR será enviada para a outra parte para notificar a outra parte. Quando uma mensagem do tipo PYTPE_ERROR for recebida, _error_dispatch será chamado, error_source será registrado em dead_service, e error_session será registrado em 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

No final da suspensão, dispatch_error_queue é chamado para processar error_queue, o co em espera é encontrado na sessão e, em seguida, é encerrado à força para garantir que o co não estará ocupado esperando o tempo todo.

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

O fluxo de uma solicitação de rpc sincronizada é o seguinte. Quando o co atual de um serviço é suspenso, os processos de outros cos no serviço podem ser executados. N cos podem ser executados de forma cruzada. A suspensão de um co não afetará a execução de outros cos, maximizando o fornecimento de poder de computação e alcançar alta simultaneidade.

 

 

Acho que você gosta

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