Mit dem Parallel Luau Programmiermodell können Sie Code auf mehreren Threads gleichzeitig ausführen, was die Leistung Ihres Erlebnisses verbessern kann. Wenn Sie Ihr Erlebnis mit mehr Inhalten erweitern, können Sie dieses Modell übernehmen, um die Leistung und Sicherheit Ihrer Luau-Skripte zu erhalten.
Modell der parallelen Programmierung
Standardmäßig werden Skripte sequentiell ausgeführt. Wenn deine Erfahrung komplexe Logik oder Inhalte hat, wie z. B. Nicht-Spieler-Charaktere (NPCs), Raycasting-Validierung und prozedurale Generierung, dann kann die sequentielle Ausführung Verzögerungen für deine Benutzer verursachen. Mit dem parallelen Modellkannst du Aufgaben in mehrere Skripte aufteilen und sie parallel ausführen. Dies macht deinen Code schneller, was
Das parallele Programmiermodell fügt Ihrem Codesauch Sicherheitsvorteile hinzu. Indem Sie Code in mehrere Threads aufteilen, wirkt sich die Bearbeitung von Code in einem Thread nicht auf anderen Code aus, der parallel ausgeführt wird. Dies reduziert das Risiko, dass ein Fehler in Ihrem Code die gesamte Erlebnisbeschädigt und die Verzögerung für Benutzer auf Live-Servern minimiert, wenn Sie ein Update pushen.
Das parallele Programmiermodell bedeutet nicht, alles in mehrere Threads zu legen. Zum Beispiel setzt die Server-seitige Raycasting-Validierung jedem einzelnen Benutzer ein Remote-Ereignis parallel, erfordert aber immer noch, dass der ursprüngliche Code seriell ausgeführt wird, um globale Eigenschaften zu ändern, was ein häufiges Muster für die parallele Ausführung ist.
In den meisten Fällen musst du serielle und parallele Phasen kombinieren, um deine gewünschte Ausgabe zu erzielen, seitdem es derzeit einige Operationen gibt, die nicht parallel unterstützt werden und die Ausführung von Skripten verhindern können, z. B. das Ändern von Instanzen in parallelen Phasen. Für weitere Informationen über die Ebene der parallelen Verwendung von APIs, siehe Thread-Sicherheit.
Code in mehrere Threads aufteilen
Um die Skripte deines Erlebnisses in mehreren Threads gleichzeitig auszuführen, musst du sie in logische Chunks unter verschiedenen Schauspielern im Datenmodell aufteilen. Schauspieler werden durch Actor Instanzen repräsentiert, die von 2> Class.DataModel2> erben. Sie arbeiten als Einheiten der Ausführungsisolation, die die Belastung auf mehr
Akteur-Instanzen platzieren
Du kannst Akteure in richtige Container legen oder sie verwenden, um die Top-Level-Instanz-Typen deiner 3D-Entitäten wie NPCs und Strahlenwerfer zu ersetzen, dann entsprechende Skripte hinzufügen.
In den meisten Situationen solltest du einen Akteur nicht als Kind eines anderen Akteurs im Modellplatzieren. Wenn du dich jedoch dafür entscheidest, ein Skript in mehreren Akteuren für deinen spezifischen Anwendungsfall verschachtelt zu platzieren, ist das Skript im Besitz seines nächsten Vorfahren.
Desynchronisieren Threads
Obwohl das Setzen von Skripten unter Akteure ihnen die Fähigkeit zur parallelen Ausführung gewährt, wird der Code standardmäßig immer noch auf dem einzelnen Thread seriell ausgeführt, was die Erfüllungnicht verbessert. Sie müssen die task.desynchronize(), eine nachgiebige Funktion, die die Ausführung der aktuellen Coroutine für die parallele Ausführung von Code ausset
Alternativ kannst du die Methode Datatype.RBXScriptSignal:ConnectParallel() verwenden, wenn du einen Signal-Callback planen möchtest, um deinen Code sofort nach dem Auslösen parallel auszuführen. Du musst Library.task.desynchronize() nicht innerhalb des Signal-Callbacks aufrufen.
Thread desynchronisieren
local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- Ein paralleler Code, der eine Updateberechnet
task.synchronize()
... -- Einige serielle Code, der den Zustand der Instanzen ändert
end)
Skripte, die Teil desselben Akteurs sind, werden immer nacheinander ausgeführt, sodass Sie mehrere Akteure benötigen. Wenn Sie beispielsweise alle parallel aktivierten Verhaltensskripte für Ihren NPC in einem Schauspieler platzieren, laufen sie immer noch seriell auf einem einzigen Thread, aber wenn Sie mehrere Akteure für unterschiedliche NPC-Logik haben, läuft jeder von ihnen parallel auf seinem eigenen Thread. Weitere Informationen finden Sie unter Best Practices.
Thread-Sicherheit
Während der parallelen Ausführung kannst du auf die meisten Instanzen der DataModel Hierarchie wie üblich auf sie zugreifen, aber einige API-Eigenschaften und -Funktionen sind nicht sicher zu lesen oder zu schreiben. Wenn du sie in deinem parallelen Codesverwendest, kann die Roblox-Engine diese Zugriffe automatisch erkennen und verhindern.
Die API-Mitglieder haben ein Thread-Safety-Level, das anzeigt, ob und wie Sie sie in Ihrem parallelen Codesverwenden können, wie die folgende Tabelle zeigt:
Sicherheitsniveau | Für Eigenschaften | Für Funktionen |
---|---|---|
Unsicher | Kann nicht parallel gelesen oder geschrieben werden. | Kann nicht parallel aufgerufen werden. |
Parallel lesen | Kann gelesen, aber nicht parallel geschrieben werden. | N/A |
Lokaler Safe | Kann innerhalb desselben Akteurs verwendet werden; kann von anderen Actors parallel gelesen, aber nicht geschrieben werden. | Kann innerhalb desselben Akteurs aufgerufen werden; kann nicht von anderen Actors parallel aufgerufen werden. |
Sicher | Kann gelesen und geschrieben werden. | Kann abgerufen werden. |
Du kannst Thread-Safety-Tags für API-Mitglieder auf der API-Referenz finden. Wenn du sie verwendest, solltest du auch berücksichtigen, wie API-Aufrufe oder Eigenschaftsänderungen zwischen parallelen Threads interagieren können. Normalerweise ist es für mehrere Akteure sicher, die gleichen Daten wie andere Akteure zu lesen, aber den Zustand anderer Akteure nicht zu ändern.
Cross-Thread-Kommunikation
Im Multithreading-Kontext können Sie Skripts in verschiedenen Schauspielern immer noch erlauben, um Daten auszutauschen, Aufgaben zu koordinieren und Aktivitäten zu synchronisieren. Die Engine unterstützt die folgenden Mechanismen für die Cross-Thread-Kommunikation:
- Actor-Messaging API zum Senden von Nachrichten an einen Akteur mit Skripten.
- Gemeinsame Tabelle Datenstruktur zum effizienten Teilen einer großen Menge an Daten zwischen mehreren Akteuren in einem gemeinsamen Zustand.
- Direkte Datenmodell-Kommunikation für einfache Kommunikation mit Einschränkungen.
Sie können mehrere Mechanismen unterstützen, um Ihre Anforderungen an die parallele Kommunikation anzupassen. Zum Beispiel können Sie eine gemeinsame Tabelle über die Actor Messaging-API senden.
Actor Messaging
Die Actor-Messaging-API ermöglicht es einem Skript, das. PL: die Skripts, entweder in einem seriellen oder parallelen Kontext, Daten an einen Akteur im selben Modellzu senden. Die Kommunikation über diese API ist asynchron, in der der Sender nicht blockiert, bis der Empfänger die Nachricht erhält.
Wenn Sie Nachrichten mit dieser API senden, müssen Sie ein Thema für die Kategorisierung der Nachricht definieren. Jede Nachricht kann nur an einen einzelnen Akteur gesendet werden, aber dieser Akteur kann intern mehrere Rückrufe haben, die an eine Nachricht gebunden sind. Nur Skripte, die Nachkommen eines Akteurs sind, können Nachrichten empfangen.
Die API hat die folgenden Methoden:
- Actor:SendMessage() für das Senden einer Nachricht an einen Akteur.
- Actor:BindToMessage() für das Binden eines Luau-Callbacks an eine Nachricht mit dem angegebenen Thema in einem seriellen Kontext.
- Actor:BindToMessageParallel() für das Binden eines Luau-Callbacks an eine Nachricht mit dem angegebenen Thema in einem parallelen Kontext.
Das folgende Beispiel zeigt, wie man Actor:SendMessage() benutzt, um ein Thema zu definieren und eine Nachricht an den Absender zu senden:
Beispiel-Nachrichten-Sender
-- Senden Sie zwei Nachrichten an den Arbeiter-Akteur mit dem Thema "Begrüßung"local workerActor = workspace.WorkerActorworkerActor:SendMessage("Greeting", "Hello World!")workerActor:SendMessage("Greeting", "Welcome")print("Sent messages")
Das folgende Beispiel zeigt, wie man Actor:BindToMessageParallel() benutzt, um einen Rückruf für ein bestimmtes Thema in einem parallelen Kontext auf der beendenzu binden:
Beispiel-Nachrichten-Empfänger
-- Holen Sie sich den Akteur, der diesem Skript zugewiesen ist
local actor = script:GetActor()
-- Binden Sie einen Rückruf für das Thema "Begrüßung"
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("Bound to messages")
Geteilte Tabelle
SharedTable ist eine tabellenartige Datenstruktur, die von Skripten aus zugänglich ist, die unter mehreren Schauspielern laufen. Es ist nützlich für Situationen, die eine große Menge an Daten beinhalten und einen gemeinsamen Zustand zwischen mehreren Threads erfordern. Zum Beispiel, wenn mehrere Schauspieler an einem gemeinsamen Weltzustand arbeiten, der nicht im Modellgespeichert ist.
Das Senden einer freigegebenen Tabelle an einen anderen Akteur erstellt keine Kopie der Daten. Stattdessen ermöglichen freigegebene Tabellen sichere und atomare Updates durch mehrere Skripte gleichzeitig. Jede Aktualisierung einer freigegebenen Tabelle durch einen Akteur ist sofort für alle Akteure sichtbar. Gemeinsame Tabellen können auch in einem ressourceneffizienten Prozess geklont werden, der die strukturelle Freigabe verwendet, anstatt die zugrunde liegenden Daten zu kopieren.
Direkte Datenmodell-Kommunikation
Sie können auch die Kommunikation zwischen mehreren Threads direkt mit dem Modellerleichtern, in dem verschiedene Akteure Eigenschaften oder Attribute schreiben und anschließend lesen können. Um jedoch die Thread-Sicherheit zu gewährleisten, können parallel laufende Skripte im Allgemeinen nicht in das Modellschreiben. Daher ist die direkte Verwendung des Datenmodells für die Kommunikation mit Einschränkungen verbunden und kann Skripte dazu zwingen, häufig zu synchronisieren, was die Leistung Ihrer Skripte beeinträcht
Beispiele
Server-seitige Raycasting-Validierung
Für ein Kampf- und Erlebnismusst du Raycasting für die Waffen deiner Benutzer aktivieren. Mit der Client-Simulation der Waffen, um eine gute Latenz zu erreichen, muss der Server den Treffer bestätigen, was Raycasts und einige Heuristiken beinhaltet, die die erwartete Zeichengeschwindigkeit berechnen und das vergangene Verhalten betrachten.
Statt ein einzelnes zentralisierten Skript zu verwenden, das sich mit einem Remote-Ereignis verbindet, das Clients verwenden, um Trefferinformationen zu kommunizieren, können Sie jeden Treffer-Validierungsprozess auf der Serverseite parallel ausführen, wobei jeder Benutzercharakter ein separates Remote-Ereignis hat.
Das serverseitige Skript, das unter dem Charakter Actor läuft, verbindet sich mit diesem Remote-Ereignis mit einer parallelen Verbindung, um die relevanten Logik für die Bestätigung des Treffers auszuführen. Wenn die Logik eine Bestätigung eines Treffers findet, wird der Schaden abgezogen, was das Ändern von Eigenschaften beinhaltet, so dass es anfangs seriell ausgeführt wird.
local tool = script.Parent.Parent
local remoteEvent = Instance.new("RemoteEvent") -- Erstellen Sie ein neues Remote-Ereignis und überlassen Sie es dem Tool
remoteEvent.Name = "RemoteMouseEvent" -- Benennen Sie es, damit das lokale Skript nach ihm suchen kann
remoteEvent.Parent = tool
local remoteEventConnection -- Erstellen Sie eine Referenz für die Remote-Ereignisverbindung
-- Funktion, die auf ein Remote-Ereignis hört
local function onRemoteMouseEvent(player: Player, clickLocation: CFrame)
-- SERIAL: Führen Sie den Setup-Code in seriell aus
local character = player.Character
-- Ignoriere den Charakter des Benutzers während des Raycastings
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
-- PARALLEL: Führen Sie die Raycast in paralleler Ausführung aus
task.desynchronize()
local origin = tool.Handle.CFrame.Position
local epsilon = 0.01 -- Wird verwendet, um den Strahl leicht zu verlängern, da die Klicke位ierung etwas vom Objekt abweichen kann
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: Der Code unten modifiziert den Zustand außerhalb des Akteurs
task.synchronize()
explosion.DestroyJointRadiusPercent = 0 -- Mache die Explosion nicht-letal
explosion.Position = clickLocation.Position
-- Mehrere Akteure könnten dieselbe Teil in einem Raycast erhalten und sich entscheiden, sie zu zerstören
-- Das ist perfekt sicher, aber es würde zwei Explosionen auf einmal statt einer erzeugen
-- Die folgenden doppelten Checks, dass die Ausführung zuerst an diesen Teil kam
if hitPart.Parent then
explosion.Parent = workspace
hitPart:Destroy() -- Zerstöre es
end
end
end
end
-- Verbinden Sie das Signal zuerst seriell, da einige Einrichtungscodes nicht parallel ausgeführt werden können
remoteEventConnection = remoteEvent.OnServerEvent:Connect(onRemoteMouseEvent)
Server-seitige Prozedurielle Geländenerzeugung
Um eine riesige Welt für deine Erlebniszu erstellen, kannst du die Welt dynamisch bevölkern. Die prozedurale Generierung erstellt in der Regel unabhängige Terrain-Chunks, wobei der Generator relativ komplizierte Berechnungen für die Platzierung von Objekten, den Materialeinsatz und die Voxel-Füllung durchführt. Das parallele Ausführen des Generierungscodes kann die Effizienz des Prozesses verbessern. Das folgende Codebeispiel dient als Beispiel.
-- Die parallele Ausführung erfordert den Einsatz von Akteuren
-- Dieses Skript klont sich; das Original initiiert den Prozess, während die Klone als Arbeiter agiert
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
-- Alle Akteure unter Selbst
for _, actor in workers do
actor.Parent = script
end
-- Weisen Sie die Akteure, Terrain durch das Senden von Nachrichten zu generieren
-- In diesem Beispiel werden Akteure zufällig ausgewählt
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)
-- Aus dem ursprünglichen Skript, das. PL: die Skriptsaussteigen; der Rest des Codes läuft in jedem Akteur
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
-- Binden Sie den Rückruf, der parallel ausgeführt werden soll, in den Ausführungskontext
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)
-- Derzeit muss WriteVoxels() in der seriellen Phase aufgerufen werden
task.synchronize()
workspace.Terrain:WriteVoxels(
Region3.new(corner, corner + Vector3.new(16, 16, 16)),
4,
voxels.materials,
voxels.occupancy
)
end)
Best Practices
Um die maximalen Vorteile der parallelen Programmierung anzuwenden, beachten Sie die folgenden Best Practices, wenn Sie Ihren Codeshinzufügen:
Vermeiden Sie lange Berechnungen — Selbst parallel können lange Berechnungen die Ausführung anderer Skripte blockieren und Verzögerungen verursachen. Vermeiden Sie die parallele Programmierung, um eine große Menge langer, unnachgiebiger Berechnungen zu verarbeiten.