并行 Luau

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

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

并行编程模型

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

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

采用并行编程模型并不意味着要将所有内容放入多个线程中。例如,服务器端射线投射验证 为每个用户设置一个远程事件以并行方式运行,但仍然需要初始代码以串行方式运行以更改全局属性,这是并行执行的常见模式。

大多数情况下,您需要将串行阶段和并行阶段结合起来,以实现所需的输出,因为目前有一些不支持并行的操作会阻止脚本运行,例如在并行阶段修改实例。有关并行中 API 使用级别的更多信息,请参见 线程安全性

将代码拆分为多个线程

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

放置演员实例

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

演员下的脚本示例

在大多数情况下,您不应将演员作为另一个演员的数据模型子项。然而,如果您决定根据具体用例将脚本嵌套在多个演员中,则脚本由其最近的祖先演员拥有。

显示脚本由其最近演员拥有的演员和脚本树

线程异步化

尽管将脚本放在演员下使它们具备了并行执行的能力,但默认情况下代码仍然是在单个线程上串行运行的,这并不能提高运行时性能。您需要调用 task.desynchronize(),这是一个可暂停的函数,它可以挂起当前协同程序的执行以便并行运行代码,并在下一个并行执行的机会时恢复执行。要将脚本切换回串行执行,请调用 task.synchronize()

或者,当您希望在触发时立即并行运行代码时,可以使用 RBXScriptSignal:ConnectParallel() 方法来调度信号回调。您无需在信号回调中调用 task.desynchronize()

异步化线程

local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- 一些并行代码来计算状态更新
task.synchronize()
... -- 一些串行代码来更改实例的状态
end)

属于同一演员的脚本总是按照顺序执行,因此您需要多个演员。例如,如果将所有并行启用的行为脚本放在一个演员中,它们依然会在单个线程上串行运行,但是如果您为不同的 NPC 逻辑使用多个演员,每个演员都会在自己的线程上并行运行。有关更多信息,请参见 最佳实践

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

线程安全性

在并行执行过程中,您仍然可以像往常一样访问 DataModel 层次结构中的大多数实例,但有些 API 属性和函数在读取或写入时并不安全。如果您在并行代码中使用它们,Roblox 引擎可以自动检测并防止这些访问的发生。

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

安全级别对于属性对于函数
不安全不能在并行中读取或写入。不能在并行中调用。
可并行读取可以读取但不能在并行中写入。N/A
本地安全可以在同一演员内部使用;可以读取但不能被其他 Actors 在并行中写入。可以在同一演员内部调用;不能被其他 Actors 在并行中调用。
安全可以读取和写入。可以被调用。

您可以在 API 参考 中找到 API 成员的线程安全标签。在使用它们时,您还应该考虑 API 调用或属性更改可能在并行线程之间如何交互。通常多个演员可以安全地读取相同的数据,但不能修改其他演员的状态。

跨线程通信

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

您可以支持多种机制以满足您的跨线程通信需求。例如,您可以通过演员消息传递 API 发送共享表。

演员消息传递

演员消息传递 API 允许脚本(无论是串行或并行上下文)向同一数据模型中的一个演员发送数据。通过此 API 的通信是异步的,发送方在接收方收到消息之前不会被阻塞。

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

该 API 有以下方法:

以下示例显示了如何使用 Actor:SendMessage() 在发送方端定义主题并发送消息:

示例消息发送者

local Workspace = game:GetService("Workspace")
-- 向工作者演员发送两个主题为 "Greeting" 的消息
local workerActor = Workspace.WorkerActor
workerActor:SendMessage("Greeting", "Hello World!")
workerActor:SendMessage("Greeting", "Welcome")
print("发送消息")

以下示例显示了如何使用 Actor:BindToMessageParallel() 在接收方端为某个主题绑定回调:

示例消息接收者

-- 获取该脚本所属的演员
local actor = script:GetActor()
-- 为 "Greeting" 消息主题绑定回调
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("已绑定到消息")

共享表

SharedTable 是一个类表的数据结构,可由在多个演员下运行的脚本访问。它适用于在多个线程之间需要大量数据以及共享状态的情况。例如,当多个演员在不存储在数据模型中的共同的世界状态下工作时。

将共享表发送给另一个演员并不会创建数据的副本。相反,共享表允许多个脚本同时的安全和原子更新。一旦一个演员对共享表进行了任何更新,所有演员都会立即看到这些更改。共享表还可以通过利用结构共享而不是复制底层数据的过程以资源高效的方式进行克隆。

直接数据模型通信

您还可以通过数据模型直接促进多个线程之间的通信,在这种情况下,不同演员可以写入和随后的读取属性或属性。然而,为了保持线程安全,通常在并行运行的脚本不能写入数据模型。因此,直接使用数据模型进行通信带有限制,并可能迫使脚本频繁地同步,这会影响脚本的性能。

示例

服务器端射线投射验证

对于战斗和战斗体验,您需要为用户的武器启用 射线投射。由于客户端模拟武器以取得良好的延迟,服务器必须确认击中,这涉及到进行射线投射和一些计算预期角色速度的启发式,并查看过去的行为。

与其使用连接到客户端用以沟通击中信息的远程事件的单个集中式脚本,您可以在服务器端并行运行每个击中验证过程,为每个用户角色使用一个单独的远程事件。

在该角色的 Actor 下运行的服务器端脚本通过并行连接连接到此远程事件以运行相关的逻辑以确认击中。如果逻辑找到击中的确认,则扣除伤害,这涉及更改属性,因此它最初串行运行。


local Workspace = game:GetService("Workspace")
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)

服务器端程序生成地形

要为您的体验创建一个广阔的世界,您可以动态填充世界。程序生成通常创建独立的地形块,生成器执行较为复杂的对象放置、材料使用和体素填充计算。并行运行生成代码可以提高流程的效率。以下代码示例就是一个示例。


-- 并行执行需要使用演员
-- 此脚本克隆自身;原始进行过程,克隆作为工作者
local Workspace = game:GetService("Workspace")
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)

最佳实践

为了最大限度地发挥并行编程的好处,在添加 Luau 代码时,请参考以下最佳实践:

  • 避免长时间计算 — 即使在并行中,长时间计算也会阻塞其他脚本的执行并导致延迟。避免使用并行编程处理大量长时间、不可等待的计算。

    演示如何过载并行执行阶段仍然可能导致延迟的图示
  • 使用合适数量的演员 — 为了获得最佳性能,请使用更多的 Actors。即使设备的核心数少于 Actors,细粒度的分裂也可以更高效地在核心之间进行负载平衡。

    演示如何使用更多演员在核心之间平衡负载

    这并不是说您应该使用尽可能多的 Actors。您仍然应该根据逻辑单元将代码划分为 Actors,而不是将连接逻辑切割成不同的 Actors。例如,如果您想在并行中启用 射线投射验证,合理的做法是使用64个 Actors 甚至更多,而不是仅仅使用 4 个,即使您针对 4 核系统。这样有助于提高系统的可扩展性,并允许系统根据底层硬件的能力分配工作。然而,您也不应使用过多的 Actors,以免难以维护。

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。