並行 Luau

*此內容是使用 AI(Beta 測試版)翻譯,可能含有錯誤。若要以英文檢視此頁面,請按一下這裡

使用 並行 Luau 編程模型,您可以同時在多個線程上執行代碼,這可以提高您的體驗性能。隨著您的體驗內容擴展,您可以採用此模型來幫助維護您的 Luau 指令的性能和安全。

並列程式模型

預設情況下,指令碼會按順序執行。如果您的體驗包含複雜的邏輯或內容,例如非玩家角色 (NPC)、射線投射驗證和程序生成,則會導致順序執行時發生延遲。使用並行程式模型,您可以將 檢視任務並將它們並行運行 並將您的體驗代碼執行更快

並列程式模型也會為您的代碼增加安全好處。 通過將代碼分為多個線程,當您在一個線程編輯代碼時,編輯的代碼不會影響並列運行中的其他代碼。 這減少了您在代碼中有一個錯誤導致整個體驗的風險,並且在您推出更新時對用戶在活動伺服器上的延遲最小。

不要將並列程式模型中的所有東西都放在多個線程中。例如,服務器側射線投射驗證 為每個個人用戶設定並行的遠程事件,但仍然需要初始代碼在串行運行,以變更全球屬性,這是並列執行的常見模式。

大多數時候,您需要結合串行和並行階段來達到所需的輸出,因為目前有一些操作不支持並行,例如在並行階段修改實例。有關並行 API 使用程度的更多資訊,請參閱線程安全

將代碼拆分為多個線程

要在多個線程中同時執行您的體驗指令碼,您需要將它們拆分為不同 角色 在 資料模型 下的論理塊。 Actor 由 Actor 継承的 2> Class.Actor2> 實例表示。它們作為執行隔離單元,將負載分配到同一個處理

放置 Actor 實例

您可以將 actor 放在適當的容器中,或使用它們來替換 3D 實體的主要實例類型,例如 NPC 和射線投射者,然後添加相應的 指令碼

An example of a Script under an Actor

對於大多數情況,您不應將演員作為其他演員的兒子在資料模型中。 如果您決定要在多個演員中呈雙層結構的脚本,脚本將屬於最親近的祖先演員。

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

取消線程同步

雖然將指令碼放在 Actor 下可以授予它們並行執行的能力,由預設情況下代碼仍然在單個線程上串行運行,這並不能提升執行時性履約。您需要調用 task.desynchronize(),這是一種可產生的函數,它暫停當前程式碼的執行以並行執行代

您也可以使用 RBXScriptSignal:ConnectParallel() 方法來安排信號回聯時立即執行您的代碼並行在發生時立即執行您的代碼。您不需要在信號回調聯內呼叫 task.desynchronize()

取消線程同步

local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- 一些並行代碼,計算狀態更新
task.synchronize()
... -- 一些可以改變狀態的串行代碼
end)

屬於同一個演員的腳本一直都是相對於彼此順序執行的,因此您需要多個演員。例如,如果您將 NPC 的所有並行啟用行為指令碼放入一個演員中,它們仍然會在單個線執行緒上串行執行,但如果您有多個對不同 NPC 語法的腳本,每個腳本都會在自己的線執行緒上並行執行。有關更多資

在單個線程中串行運行的演員中的並列代碼
在多個線程中同時執行的演員並行代碼

線程安全

在並行執行時,您可以像平常一樣存取 DataModel 階層的大多數實例,但一些 API 屬性和函數可能無法閱取或寫入。 如果您在並行代碼中使用它們,Roblox 引擎可以自動偵測並防止這些存取發生。

API 成員有一個線程安全級別,表示您是否可以在並行代碼中使用它們以及如何使用它們,如下表所示:

安全等級對於屬性對於功能
不安全 不能並行讀取或寫入。不能並行呼叫。
閱取並行 可以讀取,但不能並行寫入。
本地保險箱 可以在同一個 Actor 中使用;可以由其他 Actors 並行在線讀取,但不能寫入。可以在同一個 Actor 中呼叫;不能由其他 Actors 並行呼叫。
保險箱 可以閱取和寫入。可以呼叫。

您可以在 API 參考 找到 API 成員的線程安全標籤。當使用它們時,您應該也考慮 API 呼叫或屬性變更之間的並行線程之間的互動方式。通常來說,多個演員閱讀相同的資料,但不會修改其他演員的狀態。

跨線程通信

在多線程上下文中,您仍可以允許不同演員中的指令碼與之交流,以交換資料、協調任務和同步活動。引擎支援以下跨線程通信機制:

您可以支持多個機制來滿足您的交叉線程通信需求。例如,您可以通過 Actor Messaging API 傳送共享表。

演員傳訊

Actor Messaging API 允許串行或並行上下指令碼中的脚本發送資料到同一資料模型中的 Actor。 通過此 API 的通信是非同步的,發送方不會阻塞,直到接收方收到訊息。

使用此 API 傳送訊息時,您需要定義一個 主題 為訊息的類別。每個訊息只能發送給一個單一的演員,但這個演員可以內部有多個回餌綁定到一個訊息。只有演員的後代才能接收訊息。

API 有以下方法:

下列示例顯示了如何使用 Actor:SendMessage() 來定義主題,並在發件端發送消息:

示例訊息發件人

-- 向工作者群發送兩個訊息,主題是「敬禮」
local workerActor = workspace.WorkerActor
workerActor:SendMessage("Greeting", "Hello World!")
workerActor:SendMessage("Greeting", "Welcome")
print("Sent messages")

下列示例顯示了如何使用 Actor:BindToMessageParallel() 來為接收端的並行上下文中指定的主題提供回撥式呼叫:

示例訊息接收器

-- 取得這個指令的上一個 actor
local actor = script:GetActor()
-- 為「歡迎」訊息主題提供回覆
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("Bound to messages")

共享桌子

SharedTable 是一個類似桌子的資料結構,可以通過在多個參與者下運行的腳本存取。 它對於涉及大量資料並且需要多個線程之間的公共共享狀態的情況非常有用。 舉例來說,當多個參與者處理未儲存在資料模型中的共同世界狀態時。

將共享表發送給另一個參與者不會複製資料。 相反,共享表允許多個腳本同時進行安全和原子更新。 一個參與者對共享表的每次更新都會立即對所有參與者可見。 共享表也可以在資源高效的過程中複製,這使用結構共享而不是複製基礎資料。

直接資料模型通訊

您也可以使用資料模型直接在多個線程之間通信,在此中不同的演員可以擁有和稍後読取屬性或屬性。 但要保持線程安全,並且要求並舉行並行運行的程式碼,因此並行運行的程式碼通常無法寫入資料模型。 因此,使用直接使用資料模型通信來進行通信來進行限制,並且可能強制程式碼同步。 這可能會導

例子

伺服器端射線投射驗證

對於戰鬥和戰鬥體驗,您需要為用戶的武器啟用 射線投射 。隨著客戶端模擬武器以達到良好的延遲,服務器必須確認命中,這涉及進行射線投射和一些啟發式方法,計算預期角色速度,並查看過去行為。

而不是使用單個集中式指令碼連接到客戶端使用以傳送命中資訊的遠端事件,您可以在服務器側面並行執行每個命中驗證過程,每個用戶角色都有一個獨立的遠端事件。

在此角色的 Actor 下執行的服務器端指令碼使用並行連接連接到此遠端事件,以執行確認命中的相關論理。如果論理發現命中確認,則會扣除傷害,這涉及變更屬性,因此它最初是串行運行的。


local tool = script.Parent.Parent
local remoteEvent = Instance.new("RemoteEvent") -- 創建新的遠端事件,並將其作為工具的父級
remoteEvent.Name = "RemoteMouseEvent" -- 重命名,讓本地指令可以尋找它
remoteEvent.Parent = tool
local remoteEventConnection -- 創建一個遠端事件連接的引用
-- 聽取遠端事件的函數
local function onRemoteMouseEvent(player: Player, clickLocation: CFrame)
-- 串行:執行設置代碼在串行中
local character = player.Character
-- 在射線投射時忽略使用者的角色
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
-- 並列:以並列方式執行射線投射
task.desynchronize()
local origin = tool.Handle.CFrame.Position
local epsilon = 0.01 -- 用於在點擊位置與對物件稍微偏移一點
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")
-- 串行:以下代碼會改變狀態,以外的演員
task.synchronize()
explosion.DestroyJointRadiusPercent = 0 -- 讓爆炸變得非死
explosion.Position = clickLocation.Position
-- 多個演員可以在一個射線投射中獲得相同的部分,並決定摧毀它
-- 這是完全安全的,但將會同時引致兩次爆炸而不是一次
-- 以下雙重檢查確認執行程式碼已經到達此部分
if hitPart.Parent then
explosion.Parent = workspace
hitPart:Destroy() -- 摧毀它
end
end
end
end
-- 連接信號最初是串行的,因為一些設定代碼無法並行執行
remoteEventConnection = remoteEvent.OnServerEvent:Connect(onRemoteMouseEvent)

伺服器端程式地形生成

要為您的體驗創造廣闊的世界,您可以動態地填充世界。 程序生成通常會創建獨立的地形塊,生成器對對象放置、材料使用和體素填充執行相對複雜的計算。 並行運行生成代碼可以提高過程的效率。 以下代碼示例作為示例。


-- 並行執行需要使用 actor
-- 這個指令會自動複製;原始會啟動過程,而複製的工作人員會作為工作人員
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
-- 自己下的所有演員
for _, actor in workers do
actor.Parent = script
end
-- 指示演員發送消息以生成地形
-- 在這個範例中,演員是隨機選擇
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)
-- 從原始指令碼退出;代碼的剩餘部分在每個演員中執行
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
-- 將回潮寫入並行執行上下文
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)
-- 目前,WriteVoxels() 必須在串行階段中呼叫
task.synchronize()
workspace.Terrain:WriteVoxels(
Region3.new(corner, corner + Vector3.new(16, 16, 16)),
4,
voxels.materials,
voxels.occupancy
)
end)

最佳實踐

要應用並行程式設計的最大好處,請在新增 Lua 代碼時參考以下最佳練習:

  • 避免長度計算 — 即使是並行計算,長度計算也會導致其他指令的執行延遲。避免使用並行程式碼來處理大量長度和不屈服的計算。

    Diagram demonstrating how overloading the parallel execution phase can still cause lag
  • 使用正確的 actor 數量 — 為了獲得最佳性履約,請使用更多 Actors 。即使設備擁有少於 Actors 的核心,粒度允許在核心之間進行更有效的負載平衡。

    Demonstration of how using more actors balances the load across cores

    這不是你應該使用尽可能多的 Class