본문으로 건너뛰기

상태 통신

ControlBee의 액터들은 브로드캐스트 및 타겟 상태를 통해 상태를 통신합니다. 브로드캐스트 상태는 모든 관련 액터에게 표시되며, 타겟 상태는 특정 액터 쌍 간의 비공개 조정을 가능하게 합니다.

문제

액터들은 서로 다른 상황에서 서로 다른 통신 범위가 필요합니다. 때로는 모든 관련 액터가 사용자의 상태(_ready 또는 _error와 같은)를 보기를 원합니다. 다른 경우에는 특정 파트너와 다단계 핸드셰이크를 조정해야 하는데, 모든 신호를 브로드캐스트하면 관련 없는 액터들이 의미 없는 잡음으로 넘쳐날 것입니다. 두 특정 액터 간의 ReleasedClamp와 같은 신호는 다른 누구에게도 의미가 없습니다.

브로드캐스트 vs. 타겟 상태

ControlBee는 상태 통신을 위한 두 가지 범위를 제공합니다:

메서드범위비유
SetStatus(key, val)모든 관련 액터가 읽을 수 있음방 전체에 소리치기
SetStatusByActor(target, key, val)target만 읽을 수 있음한 사람에게 쪽지 전달하기
// 브로드캐스트 — 모든 관련 액터가 이것을 봄
SetStatus("_ready", true);

// 타겟 — actorB만 이것을 봄
SetStatusByActor(actorB, "ReadyForHandoff", true);

읽기도 동일한 방식으로 분할됩니다:

// 피어의 브로드캐스트 상태 읽기
Actor.GetPeerStatus(actorA, "_ready")

// 피어가 나를 위해 특별히 작성한 타겟 상태 읽기
Actor.GetPeerStatusByActor(actorA, "ReadyForHandoff")

각각을 사용하는 경우

브로드캐스트 상태를 사용하는 경우:

  • 여러 액터가 사용자의 상태를 알아야 하는 경우 (예: "_ready", "_busy", "_error")
  • 시스템 전체에서 모니터링 및 관찰 가능성을 활성화하려는 경우
  • 다른 액터들이 동적으로 사용자의 상태를 발견하고 반응할 수 있는 경우
  • 정보가 일반적으로 유용한 컨텍스트인 경우 (위치, 모드, 용량)

타겟 상태를 사용하는 경우:

  • 두 액터 간의 특정 다단계 핸드셰이크를 조정하는 경우
  • 신호가 하나의 특정 파트너에게만 의미가 있는 경우
  • 관련 없는 액터들이 조정 잡담으로 넘쳐나는 것을 피하고 싶은 경우
  • 양자 프로토콜을 구축하는 경우 (공급자/수신자, 잠금/해제 시퀀스)

두 패턴 모두 필수적입니다. 일반적인 액터는 일반 상태에 브로드캐스트를 사용하고 특정 조정 프로토콜에 타겟을 사용합니다.

브로드캐스트 예제

// 로봇 팔이 준비 상태를 브로드캐스트 — 모든 감독자 또는 조정자가 이것을 볼 수 있음
Actor.SetStatus("_ready", true);
Actor.SetStatus("_position", currentPosition);
Actor.SetStatus("_holdingPart", partId);

// 여러 다른 액터들이 이 상태를 모니터링할 수 있음
if (Actor.GetPeerStatus(robotArm, "_ready") is true)
{
var position = Actor.GetPeerStatus(robotArm, "_position");
var partId = Actor.GetPeerStatus(robotArm, "_holdingPart");
// 이 로봇에 작업을 요청할지 결정
}

타겟 예제 (아래 섹션에서 자세히 설명)

// 공급자와 수신자가 타겟 상태를 사용하여 인계를 조정
// 이 두 액터만 이러한 특정 조정 신호를 봄
Actor.SetStatusByActor(receiver, "ReadyForHandoff", true);
if (Actor.GetPeerStatusByActor(supplier, "Supplying") is true) { ... }

_statusScan()을 통한 반응형 실행

이 메커니즘은 반응형이지 절차적이 아닙니다 — 이것은 브로드캐스트 및 타겟 상태 모두에 적용됩니다. 블록하고 기다리는 선형 스크립트를 작성하지 않습니다. 대신:

  1. 액터 A가 상태를 설정합니다 (브로드캐스트 또는 타겟)
  2. 프레임워크가 자동으로 관련 액터에게 _status 메시지를 전달합니다
  3. 각 액터의 현재 상태는 Scan() 메서드를 호출하여 _status를 처리합니다
  4. Scan()GetPeerStatus() 또는 GetPeerStatusByActor()를 사용하여 조건을 확인하고 충족되면 진행합니다
// 액터 A — 준비되었다는 신호
Actor.SetStatusByActor(actorB, "DataReady", true);

// 액터 B — 상태의 Scan()에서 신호에 반응
private bool Scan()
{
if (Actor.GetPeerStatusByActor(actorA, "DataReady") is true)
{
ProcessData();
return true;
}
return false;
}

어떤 액터도 블록하지 않습니다. 어떤 액터도 폴링 루프에서 회전하지 않습니다. 프레임워크의 _status 메시지가 Scan()을 재평가하도록 트리거합니다.

일반적인 ProcessMessage 구조

public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case StateEntryMessage.MessageName:
Initialize();
Scan(); // 진입 시 조건 확인
return true;
case "_status":
return Scan(); // 피어 상태가 변경될 때마다 재확인
}
return false;
}

핸드셰이크 패턴

거의 모든 양자 조정은 동일한 형태를 따릅니다:

액터 A                              액터 B
──────── ────────
SetStatusByActor(B, "signal1", true) →
Scan()이 signal1을 봄
[작업 수행]
SetStatusByActor(A, "signal2", true)
← Scan()이 signal2를 봄
[작업 수행]
SetStatusByActor(B, "signal3", true) →
...

각 단계는 순차 함수의 줄이 아니라 Scan()조건입니다. 상태는 현재 단계를 추적하며 (예: _step 또는 부울 플래그를 통해), Scan()은 현재 단계에 적합한 조건을 확인합니다.

다단계 예제

// 공급자 측 — UnloadingState
private bool Scan()
{
if (_waitingForReceiver)
{
if (Actor.GetPeerStatusByActor(receiver, "WaitingSupply") is true)
{
MoveToHandoffPosition();
Actor.SetStatusByActor(receiver, "Supplying", true);
_waitingForReceiver = false;
_waitingForConfirmation = true;
}
}

if (_waitingForConfirmation)
{
if (Actor.GetPeerStatusByActor(receiver, "ReceivedProduct") is true)
{
CompleteUnloading();
return true;
}
}

return false;
}

// 수신자 측 — LoadingState
private bool Scan()
{
if (_waitingForSupply)
{
Actor.SetStatusByActor(supplier, "WaitingSupply", true);
_waitingForSupply = false;
}

if (Actor.GetPeerStatusByActor(supplier, "Supplying") is true)
{
ReceiveProduct();
Actor.SetStatusByActor(supplier, "ReceivedProduct", true);
return true;
}

return false;
}

왜 그냥 메시지를 보내지 않나요?

직접 메시지 (actor.Send(new Message(...)))도 ControlBee에서 사용되지만, 상태 통신 (브로드캐스트 및 타겟 모두)은 조정을 위한 주요 이점이 있습니다:

  • 상태는 지속적이고 쿼리 가능합니다. 액터 A가 플래그를 설정할 때 액터 B가 아직 올바른 상태에 진입하지 않았더라도 상관없습니다 — 플래그는 설정된 상태로 유지됩니다. B가 결국 상태에 진입하고 Scan()을 호출하면 플래그를 봅니다. 메시지는 B가 준비되지 않았다면 도착하고 버려질 것입니다.

  • 상태는 재스캔을 견뎌냅니다. 어떤 피어의 상태가 변경될 때마다 Scan()이 재실행됩니다. 지속적인 플래그는 순서나 경쟁 조건에 대해 걱정하지 않고 단일 Scan()에서 다른 액터의 여러 조건을 확인할 수 있음을 의미합니다.

  • 상태는 검사 가능합니다. 모든 플래그의 현재 상태는 디버깅 및 모니터링을 위해 표시됩니다. 메시지는 발사 후 망각입니다.

메시지는 일회성 명령 및 요청/응답 쌍에 사용합니다. 브로드캐스트 상태는 여러 액터가 관찰해야 하는 일반 상태에 사용합니다. 타겟 상태는 양측이 서로의 진행 상황을 확인해야 하는 양자 조정 프로토콜에 사용합니다.

Dispose()에서 정리

지속성의 반대편은 반드시 정리해야 한다는 것입니다. 상태가 종료되면 Dispose() 메서드가 설정한 모든 상태 플래그를 재설정합니다:

public override void Dispose()
{
// 브로드캐스트 상태 재설정
Actor.SetStatus("_processing", false);

// 타겟 상태 재설정
Actor.SetStatusByActor(actorB, "DataReady", false);
Actor.SetStatusByActor(actorB, "Completed", false);
}

이것이 없으면, 이전 사이클의 오래된 플래그가 다음 사이클의 Scan()을 속여서 조기에 진행하게 됩니다. 상태의 모든 SetStatus() 또는 SetStatusByActor() 호출에는 Dispose()에 해당 재설정이 있어야 합니다.

모범 사례

1. Dispose()에서 상태 플래그 정리

// ✅ 좋음 - 모든 상태 플래그 재설정
public override void Dispose()
{
Actor.SetStatus("_processing", false);
Actor.SetStatusByActor(receiver, "DataReady", false);
}

// ❌ 나쁨 - 오래된 플래그가 남음
public override void Dispose()
{
// 정리 누락 - 다음 사이클이 이전 플래그를 볼 수 있음
}

2. 일반 상태에 브로드캐스트 사용

// ✅ 좋음 - 모든 액터에게 표시되는 상태
Actor.SetStatus("_ready", true);
Actor.SetStatus("_position", currentPosition);

// ❌ 나쁨 - 일반 상태에 타겟 사용
Actor.SetStatusByActor(actorB, "_ready", true); // B만 볼 수 있음
Actor.SetStatusByActor(actorC, "_ready", true); // 각각에 대해 반복해야 함

3. 양자 조정에 타겟 사용

// ✅ 좋음 - 두 액터 간의 비공개 핸드셰이크
Actor.SetStatusByActor(receiver, "ReadyForHandoff", true);
if (Actor.GetPeerStatusByActor(supplier, "Supplying") is true) { ... }

// ❌ 나쁨 - 조정 잡담 브로드캐스트
Actor.SetStatus("ReadyForHandoff", true); // 모든 액터가 이 잡음을 봄

4. Scan()에서 _status에 반응

// ✅ 좋음 - 반응형 패턴
public override bool ProcessMessage(Message message)
{
switch (message.Name)
{
case StateEntryMessage.MessageName:
Scan();
return true;
case "_status":
return Scan(); // 피어 상태가 변경될 때 재확인
}
return false;
}

// ❌ 나쁨 - 폴링 패턴
public override bool ProcessMessage(Message message)
{
if (message.Name == TimerMessage.MessageName)
{
// 불필요한 폴링 - 대신 _status 사용
CheckPeerStatus();
}
return false;
}

5. 플래그로 핸드셰이크 단계 추적

// ✅ 좋음 - 명확한 상태 추적
private bool _waitingForReceiver = true;
private bool _waitingForConfirmation = false;

private bool Scan()
{
if (_waitingForReceiver)
{
if (Actor.GetPeerStatusByActor(receiver, "Ready") is true)
{
_waitingForReceiver = false;
_waitingForConfirmation = true;
}
}
return false;
}

// ❌ 나쁨 - 불명확한 진행
private bool Scan()
{
// 진행 상황을 추적하지 않고 모든 조건 확인
if (Actor.GetPeerStatusByActor(receiver, "Ready") is true) { ... }
if (Actor.GetPeerStatusByActor(receiver, "Done") is true) { ... }
}

6. 상태 쿼리에 메시지보다 상태 선호

// ✅ 좋음 - 직접 상태 읽기
bool ready = Actor.GetPeerStatus(stage, "_ready") is true;

// ❌ 나쁨 - 왕복 메시지 쿼리
stage.Publish(new Message(this, "AreYouReady"));
// ... 응답 메시지 대기 ...

API 요약

메서드목적
SetStatus(key, val)모든 관련 액터에게 표시되는 브로드캐스트 상태 작성
GetPeerStatus(actor, key)다른 액터의 브로드캐스트 상태 읽기
SetStatusByActor(target, key, val)target에게만 표시되는 타겟 상태 작성
GetPeerStatusByActor(sender, key)sender가 나를 위해 작성한 타겟 상태 읽기

지속적인 상태 플래그 (브로드캐스트 및 타겟), 자동 _status 알림 및 반응형 Scan() 메서드의 조합은 경쟁 조건이 없고, 검사 가능하며, 액터 간의 타이밍 차이를 자연스럽게 처리하는 조정 모델을 만듭니다.

참고