并行 Luau

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

使用 Parallel Luau 编程模型,您可以同时在多个线程上运行代码,这可以提高您的体验性能。当您通过更多内容扩展您的体验时,您可以采用此模型来帮助保持 Luau 脚本的性能和安全。

并行编程模型

默认情况下,脚本按顺序执行。如果您的体验有复杂的逻辑或内容,例如非玩家角色 (NPC)、射线投射验证和程序生成,则可能会导致用户卡在。 与并行编程模型中,您可以 将任务分为多个脚本 并并行运行。这使您的体验代码运行更快,从而提高用户体验。

并行编程模型还为您的验证码添加安全性好处。通过将代码拆分为多个线程,当您在一个线程编辑代码时,它不会影响并行运行中的其他代码。这降低了您代码中一个错误的风险,并且在您推送更新时减少服务器上的用户延迟。

不要将所有东西放在多个线程中。 例如,服务器端射线投射验证 为每个单独用户设置一个远程事件并且仍然需要初始代码在串行运行以更改全球属性,这是并行执行的常见模式。

大多数时候,您需要结合串行和并行阶段来实现所需的输出,since 目前不支持并行的一些操作可能会阻止脚本运行,例如在并行阶段修改实例。有关并行 API 使用级别的更多信息,请参阅线程安全

将代码拆分为多个线程

要在多个线程中同时运行体验的脚本,您需要将它们拆分为数据模型中不同参与者下的逻辑块。 Actor 由继承自 Class.DataModel 的 Actor 实例表示。它们作为执行隔离单元,将负载分布到同时运行的多个内核上。

放置 Actor 实例

您可以将 actor 放在适当的容器中,或使用它们来替换 3D 实体的顶级实例类型,例如 NPC 和射线投射者,然后添加相应的 脚本

An example of a Script under an Actor

在数据模型中,你不应该将 actor 作为另一个 actor 的子脚本。 但如果你为特定的使用例子决定将脚本嵌套在多个 actor 中,脚本的所有权属于最近的祖先 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)

属于同一actor的脚本始终相对于彼此按顺序执行,因此您需要多个actor。例如,如果您将NPC的所有启用并行的行为脚本放入一个Actor中,它们仍然在单个线程上串行运行,但如果您有多个针对不同NPC逻辑的Actor,则每个Actor在自己的线程上并行运行。有关更多信息,请参阅最佳实践

在单个线程中串行运行的演员中的并行代码
在多个线程中同时运行的演员的并行代码

线程安全

在并行执行期间,您可以像平时 acces 到 DataModel 层级的大多数实例,但一些 API 属性和函数不是安全的读取或写入。 如果您在您的并行验证码中使用它们,Roblox 引擎可以自动检测并防止这些访问发生。

API成员有一个线程安全级别,该级别表示您是否可以在并行代验证码中使用它们以及如何使用它们,如下表所示:

安全等级对于属性对于函数
不安全 不能并行读取或写入。不能并行调用。
读取并行 可以读取,但不能并行写入。
本地保险箱 可以在同一个 Actor 中使用;可以读取,但不能并行写入另一个 Actors可以在同一个 Actor 中调用;不能由其他 Actors 并行调用。
保险箱 可以读取和写入。可以调用。

您可以在API 引用中找到 API 成员的线程安全标签。使用它们时,您应该也考虑 API 调用或属性变更之间的 API 调用或属性变更可能如何交叉。通常来说,多个参与者在阅读其他参与者的数据时是安全的,但不会修改其他参与者的状态。

跨线程通信

在多线程上下文中,您仍可以允许不同演员中的脚本相互通信,来交换数据、协调任务和同步活动。引擎支持以下跨线程通信机制:

您可以支持多个机制来满足您的跨线程通信需求。例如您可以通过 Actor Messaging API 发送一个共享表。

演员消息传递

Actor 消息传递 API 允许脚本,在串行或并行上下文中,将数据发送到同一数据模型中的 Actor。 通过此 API 的通信是异步的,发送方不会阻塞,直到接收方收到消信息。

使用此 API 发送消息时,您需要定义一个 主题 对消信息进行分类。每个消息只能发送给一个 actor,但该 actor 可以内部有多个回调绑定到一条消信息。只有 actor 子脚本可以接收消息。

API有以下方法:

下面的例子显示了如何使用 Actor:SendMessage() 来定义一个主题,并在发件人端发送消息:

示例消息发件人

-- 向工作人员 Actor 发送两个消息,其中一个是“问候”的话题
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")
-- 串行:以下代码修改了 actor 外的状态
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

    这并不意味着你应该使用尽可能多的