병렬 루아우 프로그래밍 모델을 사용하면 여러 스레드에서 동시에 코드를 실행할 수 있어 게임의 성능을 향상시킬 수 있습니다. 게임의 콘텐츠가 확장됨에 따라 이 모델을 채택하여 Luau 스크립트의 성능과 안전성을 유지할 수 있습니다.
병렬 프로그래밍 모델
기본적으로 스크립트는 순차적으로 실행됩니다. 게임에 NPC(비플레이어 캐릭터), 레이캐스팅 검증 및 절차적 생성과 같은 복잡한 논리나 콘텐츠가 있는 경우, 순차 실행은 사용자에게 지연을 초래할 수 있습니다. 병렬 프로그래밍 모델을 사용하면 작업을 여러 스크립트로 분할하고 병렬로 실행할 수 있습니다. 이렇게 하면 게임 코드가 더 빨리 실행되어 사용자 경험이 향상됩니다.
병렬 프로그래밍 모델은 코드에 안전성 이점을 추가합니다. 코드를 여러 스레드로 분할하면 한 스레드에서 코드를 수정하더라도 병렬로 실행되는 다른 코드에 영향을 미치지 않습니다. 이는 코드의 버그가 전체 게임을 손상시킬 위험을 줄이고, 업데이트를 푸시할 때 라이브 서버에서 사용자에게 지연을 최소화합니다.
병렬 프로그래밍 모델을 채택한다고 해서 모든 것을 여러 스레드에 넣어야 하는 것은 아닙니다. 예를 들어, 서버 측 레이캐스팅 검증은 각 개별 사용자에게 원격 이벤트를 병렬로 설정하지만, 여전히 전역 속성을 변경하기 위해 초기 코드는 순차적으로 실행해야 하며, 이는 병렬 실행의 일반적인 패턴입니다.
대부분의 경우 원하는 출력을 얻기 위해 순차 및 병렬 단계를 결합해야 합니다. 현재 병렬에서 지원되지 않는 일부 작업이 있어 스크립트가 실행되지 못하게 할 수 있습니다. 예를 들어, 병렬 단계에서 인스턴스를 수정하는 작업이 있습니다. 병렬에서 API 사용 수준에 대한 자세한 내용은 스레드 안전성을 참조하세요.
코드를 여러 스레드로 분할
게임의 스크립트를 여러 스레드에서 동시에 실행하려면 데이터 모델 내의 서로 다른 액터 아래에 논리적 청크로 분할해야 합니다. 액터는 Actor 인스턴스로 표현되며 DataModel에서 상속됩니다. 이들은 동시에 실행되는 여러 코어에 부하를 분산하는 실행 격리 단위로 작동합니다.
액터 인스턴스 배치
액터를 적절한 컨테이너에 배치하거나 NPC 및 레이캐스터와 같은 3D 엔티티의 최상위 인스턴스 유형을 대체하는 데 사용할 수 있으며, 그 후에 해당 스크립트를 추가합니다.

대부분의 상황에서 데이터 모델 내에서 다른 액터의 자식으로 액터를 배치하지 않아야 합니다. 그러나 특정 사용 사례를 위해 여러 액터 내에 중첩된 스크립트를 배치하기로 결정한 경우, 스크립트는 가장 가까운 조상 액터에 의해 소유됩니다.

스레드 비동기화
스크립트를 액터 아래에 배치하면 병렬 실행 기능이 부여되지만, 기본적으로 코드는 여전히 단일 스레드에서 순차적으로 실행되므로 런타임 성능이 향상되지 않습니다. task.desynchronize()를 호출해야 하며, 이는 현재 코루틴의 실행을 중단하고 병렬로 코드를 실행하기 위한 다음 병렬 실행 기회에서 다시 시작하는 yieldable 함수입니다. 스크립트를 다시 순차 실행으로 전환하려면 task.synchronize()를 호출합니다.
또는 RBXScriptSignal:ConnectParallel() 메서드를 사용하여 신호 콜백을 예약하고 트리거 시 즉시 병렬로 코드를 실행할 수 있습니다. 신호 콜백 내에서 task.desynchronize()를 호출할 필요는 없습니다.
local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- 상태 업데이트를 계산하는 일부 병렬 코드
task.synchronize()
... -- 인스턴스의 상태를 변경하는 일부 순차 코드
end)같은 액터의 일부인 스크립트는 서로에 대해 항상 순차적으로 실행되므로 여러 액터가 필요합니다. 예를 들어, NPC의 모든 병렬 가능 행동 스크립트를 하나의 액터에 넣으면 여전히 단일 스레드에서 순차적으로 실행되지만, 서로 다른 NPC 논리를 위한 여러 액터가 있으면 각 액터가 자신의 스레드에서 병렬로 실행됩니다. 자세한 내용은 모범 사례를 참조하세요.


스레드 안전성
병렬 실행 중에는 DataModel 계층의 대부분 인스턴스에 일반적으로 접근할 수 있지만, 일부 API 속성과 함수는 읽거나 쓸 수 있는 안전성이 없습니다. 병렬 코드에서 이를 사용하면 Roblox 엔진이 이러한 접근을 자동으로 감지하고 방지할 수 있습니다.
API 멤버는 스레드 안전성 수준을 가지며, 이는 병렬 코드에서 어떻게 사용할 수 있는지를 나타냅니다. 다음 표에 표시된 바와 같습니다:
| 안전성 수준 | 속성에 대한 | 함수에 대한 |
|---|---|---|
| 안전하지 않음 | 병렬로 읽거나 쓸 수 없습니다. | 병렬로 호출할 수 없습니다. |
| 병렬 읽기 | 병렬로 읽을 수 있지만 쓸 수는 없습니다. | N/A |
| 로컬 안전 | 같은 액터 내에서 사용할 수 있으며, 다른 Actors에서 병렬로 읽을 수는 있지만 쓸 수는 없습니다. | 같은 액터 내에서 호출할 수 있으며, 다른 Actors에서 병렬로 호출할 수는 없습니다. |
| 안전 | 읽고 쓸 수 있습니다. | 호출할 수 있습니다. |
API 멤버의 스레드 안전성 태그는 API 참조에서 확인할 수 있습니다. 이를 사용할 때는 API 호출이나 속성 변경이 병렬 스레드 간에 어떻게 상호작용할 수 있는지도 고려해야 합니다. 일반적으로 여러 액터가 다른 액터와 동일한 데이터를 읽는 것은 안전하지만, 다른 액터의 상태를 수정하는 것은 안전하지 않습니다.
스레드 간 통신
다중 스레드 컨텍스트에서 서로 다른 액터의 스크립트가 서로 통신하여 데이터를 교환하고 작업을 조정하며 활동을 동기화할 수 있습니다. 엔진은 스레드 간 통신을 위한 다음 메커니즘을 지원합니다:
- 스크립트를 사용하여 액터에 메시지를 보내는 액터 메시징 API.
- 여러 액터 간에 공유 상태를 효율적으로 공유하기 위한 공유 테이블 데이터 구조.
- 제한이 있는 간단한 통신을 위한 직접 데이터 모델 통신.
여러 메커니즘을 지원하여 스레드 간 통신 요구 사항을 충족할 수 있습니다. 예를 들어, 액터 메시징 API를 통해 공유 테이블을 보낼 수 있습니다.
액터 메시징
액터 메시징 API를 사용하면 스크립트가 순차 또는 병렬 컨텍스트에서 동일한 데이터 모델 내의 액터에 데이터를 보낼 수 있습니다. 이 API를 통한 통신은 비동기적이며, 발신자는 수신자가 메시지를 받을 때까지 차단되지 않습니다.
이 API를 사용하여 메시지를 보낼 때는 메시지를 분류하기 위한 주제를 정의해야 합니다. 각 메시지는 단일 액터에만 전송될 수 있지만, 해당 액터는 내부적으로 메시지에 바인딩된 여러 콜백을 가질 수 있습니다. 액터의 자식인 스크립트만 메시지를 받을 수 있습니다.
API에는 다음과 같은 메서드가 있습니다:
- Actor:SendMessage()는 액터에 메시지를 보내는 메서드입니다.
- Actor:BindToMessage()는 순차 컨텍스트에서 지정된 주제로 메시지에 Luau 콜백을 바인딩하는 메서드입니다.
- Actor:BindToMessageParallel()는 병렬 컨텍스트에서 지정된 주제로 메시지에 Luau 콜백을 바인딩하는 메서드입니다.
다음 예시는 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로 나누지 않아야 합니다. 예를 들어, 병렬로 레이캐스팅 검증을 활성화하려는 경우, 4코어 시스템을 대상으로 하더라도 64 Actors를 사용하는 것이 합리적입니다. 이는 시스템의 확장성에 유용하며, 기본 하드웨어의 능력에 따라 작업을 분산할 수 있게 합니다. 그러나 유지 관리가 어려운 너무 많은 Actors를 사용해서는 안 됩니다.