并行 Luau

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

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

并行编程模型

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

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

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

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

将代码拆分为多个线程

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

放置演员实例

您可以将演员放置在适当的容器中,或使用它们替换 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 是我们在美国及其他国家或地区的注册与未注册商标。