본문으로 건너뛰기

액터 동기화

SyncUtils.SyncRequestsCheck와 준비 상태 기반 동기화를 사용한 다중 액터 조정.

개요

ControlBee는 여러 액터가 진행하기 전에 준비 상태에 도달해야 하는 다중 액터 작업을 조정하기 위해 SyncUtils.SyncRequestsCheck를 제공합니다. 이것은 중앙 조정자(일반적으로 Syncer)가 준비 상태를 폴링하고 모든 조건이 충족되면 "go" 메시지를 보내는 다자간 랑데부 패턴을 구현합니다.

SyncRequestsCheck 패턴

핵심 개념

┌─────────┐
│ Syncer │ 중앙 조정자
└────┬────┘
│ SyncRequestsCheck를 통해 지속적으로 준비 상태 폴링

├─── 액터 A가 "ReadyToX" 상태 설정
├─── 액터 B가 "ReadyToY" 상태 설정
├─── 액터 C가 "ReadyToZ" 상태 설정

▼ 모두 준비되면

├─→ 액터 A.Send("Go")
├─→ 액터 B.Send("Go")
└─→ 액터 C.Send("Go")

메서드 시그니처

bool SyncRequestsCheck(
IActor actor, // 호출하는 액터 (Syncer)
Dictionary<string, object?> grants, // 발급된 권한을 추적
RequestSource[] requestSources, // 확인할 조건 배열
string grantName // 이 동기화 지점의 고유 식별자
)

반환값: 모든 조건이 충족되고 "go" 메시지가 전송되면 true

RequestSource 정의

new RequestSource(
targetActor, // 확인/전송할 액터
"ReadyStatusKey", // 확인할 상태 키 (예: "ReadyToLoad")
"GoMessage", // 준비되면 전송할 메시지 (예: "Load")
messagePayload // 메시지의 선택적 페이로드
)

일반적인 사용 패턴

중앙 조정자 (Syncer)에서

// AutoState.cs에서, 스캔 루프가 지속적으로 동기화 지점을 확인
private bool Scan()
{
// 동기화 지점 1: Loader에서 Table0으로 전송
if (_transferOnTableRequestId == Guid.Empty) // 중복 방지
{
if (SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(
Actor.Peers.Loader,
"ReadyToUnload", // Loader가 언로드 준비 완료
"Unload", // "Unload" 메시지 전송
new Dict { ["index"] = 0 }
),
new RequestSource(
Actor.Peers.Table0,
"ReadyToLoad", // Table0이 로드 준비 완료
"Load", // "Load" 메시지 전송
null
)
],
"UnloadToTable0" // 권한 이름
))
{
// 중복 트리거를 방지하기 위해 요청 추적
_transferOnTableRequestId = _requestId;
}
}

// 더 많은 동기화 지점...
return false;
}

참여 액터에서

각 액터는 내부 상태에 따라 지속적으로 준비 상태를 설정합니다:

// TableActor AutoState에서
private bool Scan()
{
if (!Actor.Exist.Value)
{
// 자재 없음 - 수신 준비
if (_readyToLoadId == Guid.Empty)
{
_readyToLoadId = Guid.NewGuid();
Actor.SetStatusByActor(Actor.Syncer, "ReadyToLoad", _readyToLoadId);
}
}
else if (!Actor.Trimmed.Value)
{
// 자재 있음, 미처리 - 트림 준비
if (_readyToTrimId == Guid.Empty)
{
_readyToTrimId = Guid.NewGuid();
Actor.SetStatusByActor(Actor.Syncer, "ReadyToTrim", _readyToTrimId);
}
}
else
{
// 처리됨 - 언로드 준비
if (_readyToUnloadId == Guid.Empty)
{
_readyToUnloadId = Guid.NewGuid();
Actor.SetStatusByActor(Actor.Syncer, "ReadyToUnload", _readyToUnloadId);
}
}
return false;
}

완전한 수명 주기

1단계: 준비 신호

액터들이 지속적으로 상태를 평가하고 준비 상태를 신호합니다:

Loader (자재 있음) → SetStatusByActor(Syncer, "ReadyToUnload", Guid)
Table0 (비어있음) → SetStatusByActor(Syncer, "ReadyToLoad", Guid)

2단계: Syncer 폴링

Syncer가 지속적으로 조건을 확인합니다:

SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(Loader, "ReadyToUnload", "Unload", new Dict{["index"]=0}),
new RequestSource(Table0, "ReadyToLoad", "Load", null)
],
"UnloadToTable0"
)

내부 로직:

  1. Loader.GetPeerStatusByActor(Syncer, "ReadyToUnload")가 참(Guid)인지 확인
  2. Table0.GetPeerStatusByActor(Syncer, "ReadyToLoad")가 참(Guid)인지 확인
  3. 둘 다 참이고 권한이 아직 발급되지 않았으면:
    • Loader.Send("Unload", new Dict{["index"]=0}) 전송
    • Table0.Send("Load") 전송
    • _grants 딕셔너리에 권한을 발급됨으로 표시
    • true 반환

3단계: 작업 실행

두 액터가 "go" 메시지를 동시에 수신합니다:

Loader가 "Unload" 수신 → UnloadingState 진입 → 핸드셰이크 수행
Table0이 "Load" 수신 → LoadingState 진입 → 핸드셰이크 수행

4단계: 완료 및 재설정

작업 완료 후:

// LoadingState.Dispose()에서
Actor.SetStatusByActor(Actor.Syncer, "ReadyToLoad", false); // 상태 지우기
_readyToLoadId = Guid.Empty;

// Syncer가 CycleDone 메시지를 받으면
_transferOnTableRequestId = Guid.Empty; // 요청 추적 지우기
_grants.Remove("UnloadToTable0"); // 권한 지우기

권한 관리

목적

grants 딕셔너리는 중복 트리거를 방지합니다:

private Dictionary<string, object?> _grants = new();

권한 없이: 조건이 참으로 유지되면 SyncRequestsCheck가 매 스캔 주기마다 "go" 메시지를 전송합니다.

권한 있으면: 동기화 지점에 대한 권한이 발급되면 권한이 지워질 때까지 더 이상 메시지가 전송되지 않습니다.

권한 수명 주기

// 초기 상태
_grants["UnloadToTable0"] → 존재하지 않음 → 메시지 전송, 권한 생성

// 다음 스캔 (조건이 여전히 참)
_grants["UnloadToTable0"] → 존재함 → 메시지 전송 안 함

// 완료 후
_grants.Remove("UnloadToTable0") → 권한 지워짐 → 다음 사이클 준비

요청 ID 추적

장기 실행 전송의 경우, 완료를 조정하기 위해 요청을 추적합니다:

private Guid _transferOnTableRequestId = Guid.Empty;

// 동기화 성공 시
_transferOnTableRequestId = _requestId;

// 가드 조건
if (_transferOnTableRequestId == Guid.Empty)
{
// 진행 중인 전송이 없는 경우에만 동기화 확인
if (SyncUtils.SyncRequestsCheck(...))
{
_transferOnTableRequestId = _requestId;
}
}

// 완료 시 (CycleDone 메시지 수신)
_transferOnTableRequestId = Guid.Empty;

일반적인 동기화 지점

양자 전송

패턴: 자재가 액터 A에서 액터 B로 이동

SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(actorA, "ReadyToUnload", "Unload", payload),
new RequestSource(actorB, "ReadyToLoad", "Load", null)
],
"TransferAtoB"
)

예시:

  • Loader → Table: UnloadToTable0, UnloadToTable1
  • Table → Unloader: LoadFromTable0, LoadFromTable1
  • PreLoader → Buffer: UnloadToBuffer
  • Buffer → Loader: LoadFromBuffer

3자 조정

패턴: 마스터 조정자 + 두 개의 하위 액터

SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(BufferMaster, "ReadyToSwap", "Swap", null),
new RequestSource(Buffer0, "ReadyToSwap", "ShiftBuffer", null),
new RequestSource(Buffer1, "ReadyToSwap", "ShiftBuffer", null)
],
"SwapBuffers"
)

예시: 버퍼 스왑 조정:

  • BufferMaster가 전체 스왑을 조정
  • Buffer0이 스왑 준비 완료
  • Buffer1이 스왑 준비 완료

단일 액터 작업

패턴: Syncer가 액터를 트리거하여 내부 작업 수행

SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(Table0, "ReadyToTrim", "Trim", new Dict())
],
"TrimOnTable0"
)

예시:

  • TrimOnTable0: 절단 작업 트리거
  • TrimOnTable1: 절단 작업 트리거

직접 메시징과의 비교

직접 메시지 (actor.Send)

사용 시기:

  • 일회성 명령
  • 요청/응답 쌍
  • 발사 후 망각 알림

예시:

auxiliaryActor.Send(new Message(this, "TurnOnLight"));

SyncRequestsCheck 패턴

사용 시기:

  • 여러 액터가 조정해야 함
  • 모든 당사자가 준비 상태에 도달해야 함
  • 경쟁 조건을 방지해야 함
  • 작업이 비용이 많이 들거나 되돌릴 수 없음

이점:

  • 경쟁 조건 방지 (전부 아니면 전무)
  • 중앙 조정 지점
  • 권한 시스템이 중복 트리거 방지
  • 조정자에서 시각화하기 쉬움

실제 예제: FITO 트리밍 시스템

11개의 동기화 지점이 있는 자재 흐름

[1] LoadFromElevator
LoadElevator.ReadyToTransfer + PreLoader.ReadyToLoad
→ Transfer, Load

[2] UnloadToBuffer
PreLoader.ReadyToUnload + Buffer.ReadyToLoad
→ Unload(idx), Load

[3] SwapBuffers (필요한 경우)
BufferMaster.ReadyToSwap + Buffer0.ReadyToSwap + Buffer1.ReadyToSwap
→ Swap

[4] LoadFromBuffer
Buffer.ReadyToUnload + Loader.ReadyToLoad
→ Unload, Load(idx)

[5/6] UnloadToTable0 또는 UnloadToTable1
Loader.ReadyToUnload + Table.ReadyToLoad
→ Unload(idx), Load

[7/8] TrimOnTable0 또는 TrimOnTable1
Table.ReadyToTrim
→ Trim

[9/10] LoadFromTable0 또는 LoadFromTable1
Table.ReadyToUnload + Unloader.ReadyToLoad
→ Unload, Load(idx)

[11] UnloadToElevator
Unloader.ReadyToUnload + UnloadElevator.ReadyToTransfer
→ Unload, Transfer

완전한 동기화 지점 구현

파일: fito_trimming_2025/Actors/Syncer/AutoState.cs

private bool Scan()
{
// [5] UnloadToTable0 - Loader가 Table0에 자재 전달
if (_transferOnTableRequestId == Guid.Empty)
{
if (SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(
Actor.Peers.Loader,
"ReadyToUnload",
"Unload",
new Dict { ["index"] = 0 }
),
new RequestSource(
Actor.Peers.Table0,
"ReadyToLoad",
"Load",
null
)
],
"UnloadToTable0"
))
{
_transferOnTableRequestId = _requestId;
}
}

// [7] TrimOnTable0 - Table0이 절단 작업 수행
if (SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(
Actor.Peers.Table0,
"ReadyToTrim",
"Trim",
new Dict()
)
],
"TrimOnTable0"
))
{
// 단일 액터 작업에는 요청 추적이 필요 없음
}

// [9] LoadFromTable0 - Unloader가 Table0에서 픽업
if (_transferOnTableRequestId == Guid.Empty)
{
if (SyncUtils.SyncRequestsCheck(
Actor,
_grants,
[
new RequestSource(
Actor.Peers.Table0,
"ReadyToUnload",
"Unload",
null
),
new RequestSource(
Actor.Peers.Unloader,
"ReadyToLoad",
"Load",
new Dict { ["index"] = 0 }
)
],
"LoadFromTable0"
))
{
_transferOnTableRequestId = _requestId;
}
}

return false;
}

모범 사례

1. 고유한 권한 이름 사용

// 좋음 - 구체적이고 고유함
"UnloadToTable0"
"LoadFromTable1"
"SwapBuffers"

// 나쁨 - 일반적이고 모호함
"Transfer"
"Process"
"Sync"

2. 요청 ID로 장기 작업 보호

// 중복 전송 방지
if (_transferOnTableRequestId == Guid.Empty)
{
if (SyncUtils.SyncRequestsCheck(...))
{
_transferOnTableRequestId = _requestId;
}
}

3. 상태 종료 시 상태 지우기

public override void Dispose()
{
Actor.SetStatusByActor(Actor.Syncer, "ReadyToLoad", false);
Actor.SetStatusByActor(Actor.Syncer, "ReadyToUnload", false);
_readyToLoadId = Guid.Empty;
_readyToUnloadId = Guid.Empty;
}

4. 상태 값에 Guid 사용

// 좋음 - Guid를 사용하여 잘못된 긍정 방지
if (_readyToLoadId == Guid.Empty)
{
_readyToLoadId = Guid.NewGuid();
Actor.SetStatusByActor(Actor.Syncer, "ReadyToLoad", _readyToLoadId);
}

// 나쁨 - 부울은 상태가 오래되었는지 감지할 수 없음
Actor.SetStatusByActor(Actor.Syncer, "ReadyToLoad", true);

5. 완료 후 권한 지우기

// Syncer에서, CycleDone 수신 시
case "CycleDone":
_transferOnTableRequestId = Guid.Empty;
_grants.Clear(); // 또는 선택적으로 권한 제거
return true;

디버깅 팁

동기화 지점 로깅

if (SyncUtils.SyncRequestsCheck(...))
{
Logger.Debug($"동기화 성공: {grantName}");
_requestId = ...;
}

준비 상태 검사

var loaderReady = Actor.GetPeerStatusByActor(Loader, "ReadyToUnload");
var tableReady = Actor.GetPeerStatusByActor(Table0, "ReadyToLoad");
Logger.Debug($"Loader: {loaderReady}, Table: {tableReady}");

권한 상태 모니터링

Logger.Debug($"활성 권한: {string.Join(", ", _grants.Keys)}");

요청 ID 추적

Logger.Debug($"현재 전송 요청: {_transferOnTableRequestId}");

요약

개념목적
SyncUtils.SyncRequestsCheck전부 아니면 전무 의미론으로 다중 액터 작업 조정
RequestSource한 참여자에 대한 조건(상태)과 작업(메시지) 정의
권한 딕셔너리조건이 참으로 유지되는 동안 중복 트리거 방지
요청 ID 추적장기 실행 전송의 완료 조정
Guid 기반 상태오래된 상태 감지 및 잘못된 긍정 방지
SetStatusByActor조정자에게 준비 상태 신호
GetPeerStatusByActor참여자의 준비 상태 확인

SyncRequestsCheck 패턴은 타이밍, 안전성 및 조정이 중요한 복잡한 다중 액터 작업에 대한 경쟁 조건 없는 조정을 제공합니다.

참조