Parallel Luau

*Este conteúdo é traduzido por IA (Beta) e pode conter erros. Para ver a página em inglês, clique aqui.

Com o modelo de programação Luau Paralelo , você pode executar código em vários subprocessos simultaneamente, o que pode melhorar o desempenho de sua experiência. À medida que você expande sua experiência com mais conteúdo, você pode adotar esse modelo para ajudar a manter o desempenho e a segurança de seus scripts Luau.

Modelo de Programação Paralela

Por padrão, scripts são executados sequencialmente. Se sua experiência tiver lógica ou conteúdo complexos, como personagens não jogáveis (NPCs), validação de raios e geração procedural, então a execução sequencial pode causar lag para seus usuários. Com o modelo de programação paralela, você pode dividir tarefas em vários scripts e executá-los em paralelo. Isso faz com que seu código de experiência seja executado mais rápido

O modelo de programação paralela também adiciona benefícios de segurança ao seu código. Ao dividir o código em múltiplos subprocessos, quando você edita o código em um Subprocesso, isso não afeta outros códigos executando em paralelo. Isso reduz o risco de ter um bug no seu código corrompendo toda a experiência e minimiza o atraso para os usuários em servidores ativos quando você publica uma atualização.

Adotar o modelo de programação paralela não significa colocar tudo em múltiplos subprocessos. Por exemplo, a validação de raios do lado do servidor configura para cada usuário individual um evento remoto em paralelo, mas ainda requer que o código inicial seja executado em série para alterar propriedades globais, que é um padrão comum para a execução paralela.

Na maioria das vezes, você precisa combinar fases serial e paralela para alcançar o seu saídadesejado, já que atualmente há algumas operações que não são suportadas em paralelo que podem impedir a execução de scripts, como modificar instâncias em fases paralelas. Para mais informações sobre o nível de uso de APIs em paralelo, see Segurança de Subprocesso .

Dividindo Código em Múltiplos Subprocessos

Para executar os scripts da sua experiência em múltiplos subprocessos simultaneamente, você precisa dividi-los em pedaços lógicos sob diferentes atores no modelo de dados. Atores são representados por instâncias Actor que herdam de 2> Class.DataModel2>. Eles funcionam como unidades de isolamento de execução que distribuem a carga entre vários núcleos

Colocando Instâncias de Atores

Você pode colocar atores em contêineres apropriados ou usá-los para substituir os tipos de instância de alto nível de suas entidades 3D, como NPCs e raycasters, então adicionar os scripts correspondentes scripts.

An example of a Script under an Actor

Para a maioria das situações, você não deve colocar um ator como filho de outro ator no modelo de dados. No entanto, se você decidir colocar um script aninhado em múltiplos atores para o seu caso de uso específico, o script é de propriedade de seu ator ancestral mais próximo.

A tree of actors and scripts that shows how a script is owned by its closest actor

Dessincronizando Subprocessos

Embora colocar scripts sob os atores lhes conceda a capacidade de execução paralela, por padrão, o código ainda é executado no thread único em série, o que não melhora o performancedo tempo de execução. Você precisa chamar o task.desynchronize(), uma função yieldable que suspende a execução da coroutine atual para executar o código em paralelo e ret

Alternativamente, você pode usar o método RBXScriptSignal:ConnectParallel() quando quiser agendar um retorno de chamada de sinal para executar imediatamente seu código em paralelo após o disparo. Não é necessário chamar task.desynchronize() dentro do retorno de chamada de sinal.

Dessincronizar um Subprocesso

local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- Alguns códigos paralelos que calculam uma atualização de estado
task.synchronize()
... -- Alguns códigos seriais que alteram o estado das instâncias
end)

Scripts que fazem parte do mesmo ator sempre são executados sequencialmente em relação uns aos outros, então você precisa de múltiplos atores. Por exemplo, se você colocar todos os scripts de comportamento habilitados em paralelo para o seu NPC em um ator, eles ainda serão executados em série em um único Subprocesso, mas se você tiver múltiplos atores para diferentes lógicas de NPC, cada um deles será executado em paralelo em seu próprio

Código paralelo em Atores rodando em série em um único subprocesso
Código paralelo em Atores rodando simultaneamente em vários subprocessos

Segurança do Subprocesso

Durante a execução paralela, você pode acessar a maioria das instâncias da DataModel como de costume, mas algumas propriedades e funções da API não são seguras para ler ou escrever. Se você usá-las em seu código paralelo, o motor do Roblox pode detectar e impedir automaticamente que esses acessos ocorram.

Os membros da API têm um nível de segurança de subprocesso que indica se e como você pode usá-los em seu código paralelo, como mostra a seguinte tabela:

Nível de SegurançaPara PropriedadesPara Funções
Perigoso Não pode ser lido ou escrito em paralelo.Não pode ser chamado em paralelo.
Leitura Paralela Pode ser lido, mas não escrito em paralelo.Não Aplicável
Segurança Local Pode ser usado dentro do mesmo Actor; pode ser lido, mas não escrito por outro Actors em paralelo.Pode ser chamado dentro do mesmo Actor; não pode ser chamado por outro Actors em paralelo.
Seguro Pode ser lido e escrito.Pode ser chamado.

Você pode encontrar rótulos de segurança de subprocesso para membros da API na referência da API. Ao usá-los, você também deve considerar como as chamadas da API ou as alterações de propriedade podem interagir entre subprocessos paralelos. Normalmente, é seguro para vários atores lerem os mesmos dados que outros atores, mas não modificar o estado de outros atores.

Comunicação Entre Subprocessos

No contexto de multithreading, você ainda pode permitir que scripts em diferentes atores se comuniquem entre si para trocar dados, coordenar tarefas e sincronizar atividades. O motor suporta os seguintes mecanismos para comunicação entre subprocessos:

Você pode manter vários mecanismos para acomodar suas necessidades de comunicação entre subprocessos. Por exemplo, você pode enviar uma tabela compartilhada através da API (Interface de Programação para Aplicações)de Mensageria de Ator.

Mensageria de Ator

A Mensageria de Ator API permite que um script, seja em um contexto serial ou paralelo, envie dados para um ator no mesmo modelo de dados. A comunicação através desta API é assíncrona, na qual o remetente não bloqueia até que o receptor receba a mensagem.

Ao enviar mensagens usando essa API (Interface de Programação para Aplicações), você precisa definir um tópico para categorizar a mensagem. Cada mensagem só pode ser enviada para um único ator, mas esse ator pode internamente ter vários retornos de chamada vinculados a uma mensagem. Somente scripts que são descendentes de um ator podem receber mensagens.

A API possui os seguintes métodos:

O seguinte exemplo mostra como usar Actor:SendMessage() para definir um tópico e enviar uma mensagem do terminar/parar/sairdo remetente:

Exemplo de Remetente de Mensagem

-- Envie duas mensagens para o ator trabalhador com um tópico de "Saudação"
local workerActor = workspace.WorkerActor
workerActor:SendMessage("Greeting", "Hello World!")
workerActor:SendMessage("Greeting", "Welcome")
print("Sent messages")

O seguinte exemplo mostra como usar Actor:BindToMessageParallel() para vincular um retorno de chamada para determinado tópico em um contexto paralelo no terminar/parar/sairdo receptor:

Exemplo de Receptor de Mensagem

-- Obter o ator ao qual este script está ligado
local actor = script:GetActor()
-- Vincule um retorno de chamada para o tópico "Saudação"
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("Bound to messages")

Tabela Compartilhada

SharedTable é uma estrutura de dados semelhante a uma tabela acessível a por scripts executando sob múltiplos atores. É útil para situações que envolvem uma grande quantidade de dados e exigem um estado compartilhado comum entre múltiplos subprocessos. Por exemplo, quando múltiplos atores trabalham em um estado mundial comum que não está armazenado no modelo de dados.

Enviar uma tabela compartilhada para outro ator não faz uma cópia dos dados. Em vez disso, tabelas compartilhadas permitem atualizações seguras e atômicas por múltiplos scripts simultaneamente. Cada atualização em uma tabela compartilhada por um ator é imediatamente visível a todos os atores. Tabelas compartilhadas também podem ser clonadas de forma eficiente em recursos, utilizando compartilhamento estrutural em vez de copiar os dados subjacentes.

Direta do Modelo de Dados Comunicação

Você também pode facilitar a comunicação entre vários subprocessos diretamente usando o modelo de dados, no qual diferentes atores podem escrever e subsequentemente ler propriedades ou atributos. No entanto, para manter a segurança do subprocesso, os scripts executando em paralelo geralmente não podem escrever no modelo de dados. Portanto, usar diretamente o modelo de dados para comunicação vem com restrições e pode forçar os scripts a sincronizar frequentemente, o que pode afetar o desempenho dos seus scripts.

Exemplos

Validação de Raycasting do Lado do Servidor

Para uma experiência de luta e batalha, você precisa habilitar raycasting para as armas de seus usuários. Com o cliente simulando as armas para alcançar uma boa latência, o servidor tem que confirmar o hit, o que envolve fazer raycasts e uma quantidade de heurísticas que calculam a velocidade esperada do personagem e observam comportamentos passados.

Em vez de usar um único script centralizado que se conecta a um evento remoto que os clientes usam para comunicar informações de hit, você pode executar cada processo de validação de hit no lado do servidor em paralelo, com cada personagem de usuário tendo um evento remoto separado.

O script do lado do servidor que é executado sob o Actor dele se conecta a este evento remoto usando uma conexão paralela para executar a lógica relevante para confirmar o hit. Se a lógica encontrar uma confirmação de hit, o dano é deduzido, o que envolve a alterar propriedades, então ele é executado em série inicialmente.


local tool = script.Parent.Parent
local remoteEvent = Instance.new("RemoteEvent") -- Criar um novo evento remoto e associá-lo à ferramenta
remoteEvent.Name = "RemoteMouseEvent" -- Renomeie para que o script local possa procurá-lo
remoteEvent.Parent = tool
local remoteEventConnection -- Criar uma referência para a conexão do evento remoto
-- Função que ouve um evento remoto
local function onRemoteMouseEvent(player: Player, clickLocation: CFrame)
-- SERIAL: Execute o código de configuração em série
local character = player.Character
-- Ignore o personagem do usuário ao raycasting
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
-- PARALLELO: Realize o raycast em paralelo
task.desynchronize()
local origin = tool.Handle.CFrame.Position
local epsilon = 0.01 -- Usado para estender o raio levemente, já que a localização do clique pode estar um pouco deslocada do Objeto
local lookDirection = (1 + epsilon) * (clickLocation.Position - origin)
local raycastResult = workspace:Raycast(origin, lookDirection, params)
if raycastResult then
local hitPart = raycastResult.Instance
if hitPart and hitPart.Name == "block" then
local explosion = Instance.new("Explosion")
-- SERIAL: O código abaixo modifica o estado fora do ator
task.synchronize()
explosion.DestroyJointRadiusPercent = 0 -- Torne a explosão não mortal
explosion.Position = clickLocation.Position
-- Múltiplos atores podem obter a mesma parte em um raycast e decidir destruí-la
-- Isso é perfeitamente seguro, mas resultaria em duas explosões de uma vez ao invés de uma
-- As seguintes verificações duplas verificam que a execução chegou a esta parte primeiro
if hitPart.Parent then
explosion.Parent = workspace
hitPart:Destroy() -- Destrua-o
end
end
end
end
-- Conecte o sinal em série inicialmente, pois alguns códigos de configuração não são capazes de serem executados em paralelo
remoteEventConnection = remoteEvent.OnServerEvent:Connect(onRemoteMouseEvent)

Geração de Terreno Processual do Lado do Servidor

Para criar um vasto mundo para sua experiência, você pode povoar o mundo dinamicamente. A geração processual normalmente cria pedaços de terreno independentes, com o gerador executando cálculos relativamente intrincados para a colocação de objetos, uso de materiais e preenchimento de voxel. Executar o código de geração em paralelo pode melhorar a eficiência do processo. A seguinte amostra de código serve como exemplo.


-- A execução paralela requer o uso de atores
-- Este script se clona; o original inicia o processo, enquanto os clones atuam como trabalhadores
local actor = script:GetActor()
if actor == nil then
local workers = {}
for i = 1, 32 do
local actor = Instance.new("Actor")
script:Clone().Parent = actor
table.insert(workers, actor)
end
-- Pais todos os atores sob a self
for _, actor in workers do
actor.Parent = script
end
-- Instruir os atores para gerar terrenos enviando mensagens
-- Neste exemplo, os atores são escolhidos aleatoriamente
task.defer(function()
local rand = Random.new()
local seed = rand:NextNumber()
local sz = 10
for x = -sz, sz do
for y = -sz, sz do
for z = -sz, sz do
workers[rand:NextInteger(1, #workers)]:SendMessage("GenerateChunk", x, y, z, seed)
end
end
end
end)
-- Saia do script original; o resto do código é executado em cada ator
return
end
function makeNdArray(numDim, size, elemValue)
if numDim == 0 then
return elemValue
end
local result = {}
for i = 1, size do
result[i] = makeNdArray(numDim - 1, size, elemValue)
end
return result
end
function generateVoxelsWithSeed(xd, yd, zd, seed)
local matEnums = {Enum.Material.CrackedLava, Enum.Material.Basalt, Enum.Material.Asphalt}
local materials = makeNdArray(3, 4, Enum.Material.CrackedLava)
local occupancy = makeNdArray(3, 4, 1)
local rand = Random.new()
for x = 0, 3 do
for y = 0, 3 do
for z = 0, 3 do
occupancy[x + 1][y + 1][z + 1] = math.noise(xd + 0.25 * x, yd + 0.25 * y, zd + 0.25 * z)
materials[x + 1][y + 1][z + 1] = matEnums[rand:NextInteger(1, #matEnums)]
end
end
end
return {materials = materials, occupancy = occupancy}
end
-- Vincule o retorno de chamada para ser chamado em paralelo
actor:BindToMessageParallel("GenerateChunk", function(x, y, z, seed)
local voxels = generateVoxelsWithSeed(x, y, z, seed)
local corner = Vector3.new(x * 16, y * 16, z * 16)
-- Atualmente, o WriteVoxels() deve ser chamado na fase serial
task.synchronize()
workspace.Terrain:WriteVoxels(
Region3.new(corner, corner + Vector3.new(16, 16, 16)),
4,
voxels.materials,
voxels.occupancy
)
end)

Melhores Práticas

Para aplicar os máximos benefícios da programação paralela, consulte as seguintes melhores práticas ao adicionar seu código Lua:

  • Evite cálculos longos — Mesmo em paralelo, cálculos longos podem bloquear a execução de outros scripts e causar lag. Evite usar programação paralela para lidar com um grande volume de cálculos longos e inflexíveis.

    Diagram demonstrating how overloading the parallel execution phase can still cause lag
  • Use o Número Certo de Atores — Para o melhor performance, use mais Actors . Mesmo que o dispositivo tenha menos núcleos que Actors, a granularidade permite um balanceamento de carga mais eficiente entre os núcleos.

    Demonstration of how using more actors balances the load across cores

    Isso não significa que você deva usar tantos