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

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

스레드 비동기화
스크립트를 액터 아래에 두면 병렬 실행 가능성을 부여하지만, 기본적으로 코드는 여전히 단일 스레드에서 순차적으로 실행되어 실행 성능을 개선하지 않습니다. 현재 코루틴의 실행을 중단하고 병렬로 코드를 실행하기 위해 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를 사용하여 메시지를 보낼 때는 메시지를 분류하기 위한 주제를 정의해야 합니다. 각 메시지는 단일 액터로만 전송될 수 있지만 해당 액터는 메시지에 바인딩된 여러 콜백을 가질 수 있습니다. 액터의 자손인 스크립트만 메시지를 받을 수 있습니다.
API는 다음 메서드를 제공합니다:
- Actor:SendMessage() - 액터에 메시지를 전송합니다.
- Actor:BindToMessage() - 순차 컨텍스트 내에서 지정된 주제와 함께 Luau 콜백을 메시지에 바인딩합니다.
- Actor:BindToMessageParallel() - 병렬 컨텍스트 내에서 지정된 주제와 함께 Luau 콜백을 메시지에 바인딩합니다.
다음 예제는 Actor:SendMessage()를 사용하여 주제를 정의하고 송신자 측에서 메시지를 보내는 방법을 보여줍니다:
메시지 송신자 예제local Workspace = game:GetService("Workspace")-- "Greeting" 주제로 작업자 액터에 두 개의 메시지 전송local workerActor = Workspace.WorkerActorworkerActor:SendMessage("Greeting", "안녕하세요!")workerActor:SendMessage("Greeting", "환영합니다")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를 사용해서는 안 됩니다.