[C#] Akka 중급 2-5 | 'Stash' 를 사용하여 메시지 처리 지연
- C#/Akka(Actor)
- 2022. 6. 16. 05:28
참조
소개
- 레슨 4의 마지막에 아래에서 볼 수 있듯이 라이브 차트에서 Pause/Resume 기능을 구현하는 방법에서 중대한 버그를 발견했습니다.
- 버그는 ChartingActor가 동작을 Paused로 변경하면 더이상 특정 성능 카운터에 대해 토글 버튼을 누를 때마다 생성되는 AddSeries 및 RemoveSeries 메시지를 처리하지 않는다는 것입니다.
- 현재 Form에서는, 버튼의 시각적 상태가 라이브 차트와 완전히 동기화되지 않는데 많은 시간이 걸리지 않습니다.
- 그래프가 일시 중지되고 즉시 동기화되지 않을 때 토글 버튼을 누르기만 하면 됩니다.
- 어떻게 이 문제를 고칠까요?
- 대답은 ChartingActor가 Charting 동작으로 돌아올 때까지 AddSeries 및 RemoveSeries 메시지의 처리를 연기하는 것입니다.
- 이때 실제로 해당 메시지로 무언가를 할 수 있습니다.
- 이를 위한 메커니즘이 Stash 입니다.
Key Concepts / Background
- 액터에 대한 전환 가능한 동작의 부작용 중 하나는 일부 동작이 특정 유형의 메시지를 처리할 수 없다는 것입니다.
- 예를 들어 레슨 4에서 동작 전환에 사용한 인증 예제를 살펴 보겠습니다.
Stash 가 뭔가요?
- Stash는 나중에 처리하기 위해 메시지를 연기하는 액터에 구현된 스택형(stack-like) 데이터 구조입니다.
액터에 Stash를 추가하는 방법
- 액터에 Stash를 추가하려면 다음과 같이 IWithBoundedStash 또는 IWithUnboundedStash를 인터페이스로 장식하기 만하면 됩니다.
public class UserActor : ReceiveActor, IWithUnboundedStash
{
private readonly string _userId;
private readonly string _chatRoomId;
//added along with the IWithUnboundedStash interface
public IStash Stash { get; set; }
//constructors, behaviors, etc...
}
언제 BoundedStash와 UnboundedStash가 필요한가요?
- 99%의 시간동안 UnboundedStash를 사용하기를 원할 것입니다.
- Stash가 무제한의 메시지를 수락 할 수 있도록 합니다.
- BoundedStash는 주어진 시간에 보관할 수 있는 최대 메시지 수를 설정하려는 경우에만 사용해야 합니다.
- Stash가 BoundedStash 제한을 초과할 때마다 액터가 충돌합니다.
Stash를 초기화 해야 하나요?
- UserActor에 공개 getter 와 setter를 포함하는 새로운 Stash 속성이 있습니다.
- 이것이 바로 Stash를 초기화해야 한다는 의미인가요? 아닙니다.
- Stash 속성은 액터가 로컬에서 생성 될 때마다 사용되는 Actor Construction Pipeline 이라는 Akka.NET 기능에 의해 자동으로 채워집니다.
- ActorSystem이 액터에서 IWithBoundedStash 인터페이스를 볼 때 Stash 속성 안에 BoundedStash를 자동으로 채우는 것을 알고 있습니다.
- 마찬가지로, IWithUnBoundedStash 인터페이스가 표시되면 대신 해당 속성에 UnboundedStash를 채우는 것을 알고 있습니다.
메시지 보관
- 액터의 OnReceive 또는 Receive<T> 핸들러 내에서 Stash.Stash()를 호출하여 현재 메시지를 Stash 상단에 배치할 수 있습니다.
- 지금 처리하고 싶지 않는 메시지만 보관할 필요가 있습니다.
- 아래 시각화에서, 우리 액터는 메시지 1을 기꺼이 처리하지만 메시지 2와 0은 보관합니다.
단일 메시지 보관 해제
- Stash 상단에서 메시지를 빼내기 위해 Stash.Unstash()를 호출합니다.
- Stash.Unstash()를 호출하면 Stash가 이 메시지를 다른 대기열에 있는 사용자 메시지보다 앞서 액터의 메일박스 앞에 배치합니다.
VIP 라인
- 메일 박스 안에는 액터가 처리할 사용자 메시지를 위한 별도의 두 대기열이 있는 것과 같습니다.
- 일반 메시지 대기열이 있고 VIP 라인이 있습니다.
- 이 VIP 라인은 Stash에서 오는 메시지용으로 예약되어 있으며 VIP 라인의 모든 메시지는 일반 대기열의 메시지보다 먼저 점프하여 액터가 처리됩니다.
- 참고로, 모든 사용자 메시지보다 먼저 잘라내는 시스템 메시지에 대한 슈퍼 VIP 라인도 있습니다.
한 번에 전체 Stash 해제
- 액터의 Stash에 있는 모든 것을 한꺼번에 보관 해제 해야 한다면 Stash.UnstashAll() 메서드를 사용하여 Stash의 전체 내용을 메일박스 전면으로 푸시할 수 있습니다.
메시지가 Stash에서 나올 때 원래 순서대로 유지 되나요?
- Stash에서 꺼내는 방법에 따라 다릅니다.
Stash.UnstashAll()은 FIFO 메시지 순서를 유지
- Stash.UnstashAll()을 호출할 때 Stash는 Stash에 있는 메시지의 원래 FIFO 순서가 액터의 메일박스 앞에 추가 될 때 보존되도록 합니다.
Stash.UnStash()는 메시지 순서를 변경 가능
- Stash.UnStash()를 반복해서 호출하면 메시지의 원래 FIFO 순서를 변경할 수 있습니다.
- 메일 함 내부에있는 VIP 라이늘 기억하시나요? Stash 는 메시지가 풀릴때 메시지를 넣는 곳입니다.
- 단일 메시지를 UnStash() 하면 해당 VIP 라인의 뒷면으로 이동합니다.
- 여전히 일반 사용자 메시지보다 앞서 있지만 이전에 보관되지 않았고 VIP 라인에서 앞서 있는 다른 메시지 뒤에 있습니다.
Stash에 보관한 메시지는 데이터를 잃어 버리나요?
- 아닙니다.
- 메시지를 Stash에 보관할 때 메시지와 메시지에 대한 모든 메타데이터가 포함된 Envelope 메시지를 기술적으로 저장해야 합니다.
다시 시작하는 동안 액터의 Stash에 있는 메시지는 어떻게 되나요?
- Stash는 액터의 짧은 상태 중 일부입니다.
- 다시 시작하는 경우 저장소가 파괴되고 가비지가 수집됩니다.
- 이것은 재시작 중에도 메시지가 지속되는 액터의 메일박스와 반대입니다.
- 그러나 액터의 PreRestart 수명 주기 메서드 내에서 Stash.UnstashAll()을 호출하여 재시작하는 동안 Stash의 내용을 보존할 수 있습니다.
- 이렇게 하면 모든 숨김 메시지가 다시 시작될 때까지 유지되는 액터 메일박스로 이동합니다.
//move stashed messages to the mailbox so they persist through restart
protected override void PreRestart(Exception reason, object message)
{
Stash.UnstashAll();
}
실제 시나리오 : 메시지 버퍼링을 사용한 인증
- 이제 Stash가 무엇이며 어떻게 작동하는지 알았으니 채팅방 예제에서 UserActor를 다시 찾아가 사용자가 Authenticated 되기 전에 메시지를 버리는 문제를 해결해 보겠습니다.
- 이것이 레슨 4의 개념 영역에서 디자인한 UserActor이며, 다양한 인증 상태에 대한 동작 전환이 있습니다.
public class UserActor : ReceiveActor
{
private readonly string _userId;
private readonly string _chatRoomId;
public UserActor(string userId, string chatRoomId)
{
_userId = userId;
_chatRoomId = chatRoomId;
//start with the Authenticating behavior
Authenticating();
}
protected override void PreStart()
{
//start the authentication process for this user
Context.ActorSelection("/user/authenticator/")
.Tell(new AuthenticatePlease(_userId));
}
private void Authenticating()
{
Receive<AuthenticationSuccess>(auth => {
Become(Authenticated); //switch behavior to Authenticated
});
Receive<AuthenticationFailure>(auth => {
Become(Unauthenticated); //switch behavior to Unauthenticated
});
Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
inc => {
//can't accept message yet - not auth'd
});
Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
inc => {
// can't send message yet - not auth'd
});
}
private void Unauthenticated()
{
//switch to Authenticating
Receive<RetryAuthentication>(retry => Become(Authenticating));
Receive<IncomingMessage>(int => inc.ChatRoomId == _chatRoomId,
inc => {
//have to reject message - auth failed
});
Receive<OutgoingMessage>(int => inc.ChatRoomId == _chatRoomId,
inc => {
//have to reject message - auth failed
})
}
private void Authenticated()
{
Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
inc => {
// print message for user
});
Receive<OutgoingMessage>(inc => inc.ChatRoomId == _chatRoomId,
inc => {
// send message to chatroom
});
}
}
- 레슨 4에서 채팅방 UserActor 예제를 처음 보았을 때, 처음에 인증을 활성화하기 위해 동작을 전환하는데 중점을 두었습니다.
- 그러나 우리는 UserActor의 주요 문제를 무시했습니다.
- Authenticating 단계에서 시도한 OutgoingMessage와 IncomingMessage 인스턴스를 모두 버리기만 하면 됩니다.
- 메시지 처리를 지연시키는 방법을 알지 못했기 때문에 아무런 이유 없이 사용자에게 메시지가 손실되고 있습니다.
- 이러한 메시지를 처리하는 올바른 방법은 UserActor가 Authenticated 또는 Unauthenticated 상태가 될 때까지 임시로 저장하는 것입니다.
- 이때 UserActor는 사용자와 주고받는 메시지로 무엇을 할 것인지에 대해 현명한 결정을 내릴 수 있습니다.
- 사용자가 인증되었는지 여부를 알 때까지 메시지 처리를 지연하도록 UserActor의 Authenticating 동작을 업데이트하면 다음과 같습니다.
public class UserActor : ReceiveActor, IWithUnboundedStash
{
//constructors, fields, etc...
private void Authenticating()
{
Receive<AuthenticationSuccess>(auth =>
{
Become(Authenticated); //switch behavior to Authenticated
//move all stashed messages to the mailbox for processing in new behavior
Stash.UnstashAll();
});
Receive<AuthenticationFailure>(auth => {
Become(Unauthenticated); //switch behavior to Unauthenticated
//move all stashed messages to the mailbox for processing in new behavior
Stash.Unstashall();
});
Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
inc => {
//save this message for later
Stash.Stash();
});
Receive<OutgoingMessage>(int => inc.ChatRoomId == _chatRoomId,
inc => {
//save this message for later
Stash.Stash();
});
}
}
실습
- ButtonToggleActor.cs
using Akka.Actor;
using System.Windows.Forms;
namespace Akka_Test.Actors
{
public class ButtonToggleActor : UntypedActor
{
public class Toggle { }
private readonly CounterType _myCounterType;
private bool _isToggledOn;
private readonly Button _myButton;
private readonly IActorRef _coordinatorActor;
public ButtonToggleActor(IActorRef coordinatorActor, Button myButton,
CounterType myCounterType, bool isToggledOn = false)
{
_coordinatorActor = coordinatorActor;
_myButton = myButton;
_isToggledOn = isToggledOn;
_myCounterType = myCounterType;
}
protected override void OnReceive(object message)
{
if (message is Toggle && _isToggledOn)
{
// toggle is currently on
// stop watching this counter
_coordinatorActor.Tell(
new PerformanceCounterCoordinatorActor.Unwatch(_myCounterType));
FlipToggle();
}
else if (message is Toggle && !_isToggledOn)
{
// toggle is currently off
// start watching this counter
_coordinatorActor.Tell(
new PerformanceCounterCoordinatorActor.Watch(_myCounterType));
FlipToggle();
}
else
{
Unhandled(message);
}
}
private void FlipToggle()
{
// flip the toggle
_isToggledOn = !_isToggledOn;
// change the text of the button
_myButton.Text = $"{_myCounterType.ToString().ToUpperInvariant()} {(_isToggledOn ? "ON" : "OFF")}";
}
}
}
- ChartingActor.cs
using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;
using System.Windows.Forms;
namespace Akka_Test.Actors
{
public class ChartingActor : ReceiveActor, IWithUnboundedStash
{
public const int MaxPoints = 250;
public int xPosCounter = 0;
public IStash Stash { get; set; }
public class InitializeChart
{
public Dictionary<string, Series> InitialSeries { get; private set; }
public InitializeChart(Dictionary<string, Series> initialSeries)
{
InitialSeries = initialSeries;
}
}
public class TogglePause { }
public class AddSeries
{
public Series Series { get; private set; }
public AddSeries(Series series)
{
Series = series;
}
}
public class RemoveSeries
{
public string SeriesName { get; private set; }
public RemoveSeries(string seriesName)
{
SeriesName = seriesName;
}
}
private readonly Chart _chart;
private readonly Button _pauseButton;
private Dictionary<string, Series> _seriesIndex;
public ChartingActor(Chart chart, Button pauseButton) : this(chart, new Dictionary<string, Series>(), pauseButton)
{
}
public ChartingActor(Chart chart, Dictionary<string, Series> seriesIndex, Button pauseButton)
{
_chart = chart;
_seriesIndex = seriesIndex;
_pauseButton = pauseButton;
Charting();
}
private void Charting()
{
Receive<InitializeChart>(ic => HandleInitialize(ic));
Receive<AddSeries>(addSeries => HandleAddSeries(addSeries));
Receive<RemoveSeries>(removeSeries => HandleRemoveSeries(removeSeries));
Receive<Metric>(metric => HandleMetrics(metric));
// new receive handler for the TogglePause message type
Receive<TogglePause>(pause =>
{
SetPauseButtonText(true);
BecomeStacked(Paused);
});
}
private void HandleAddSeries(AddSeries series)
{
if (!string.IsNullOrEmpty(series.Series.Name) &&
!_seriesIndex.ContainsKey(series.Series.Name))
{
_seriesIndex.Add(series.Series.Name, series.Series);
_chart.Series.Add(series.Series);
SetChartBoundaries();
}
}
private void HandleRemoveSeries(RemoveSeries series)
{
if (!string.IsNullOrEmpty(series.SeriesName) &&
_seriesIndex.ContainsKey(series.SeriesName))
{
var seriesToRemove = _seriesIndex[series.SeriesName];
_seriesIndex.Remove(series.SeriesName);
_chart.Series.Remove(seriesToRemove);
SetChartBoundaries();
}
}
private void HandleMetrics(Metric metric)
{
if (!string.IsNullOrEmpty(metric.Series) &&
_seriesIndex.ContainsKey(metric.Series))
{
var series = _seriesIndex[metric.Series];
series.Points.AddXY(xPosCounter++, metric.CounterValue);
while (series.Points.Count > MaxPoints) series.Points.RemoveAt(0);
SetChartBoundaries();
}
}
private void HandleMetricsPaused(Metric metric)
{
if(!string.IsNullOrEmpty(metric.Series)
&& _seriesIndex.ContainsKey(metric.Series))
{
var series = _seriesIndex[metric.Series];
//set the Y value to zero when we're paused
series.Points.AddXY(xPosCounter++, 0.0d);
while (series.Points.Count > MaxPoints) series.Points.RemoveAt(0);
SetChartBoundaries();
}
}
private void HandleInitialize(InitializeChart ic)
{
if (ic.InitialSeries != null)
{
// swap the two series out
_seriesIndex = ic.InitialSeries;
}
// delete any existing series
_chart.Series.Clear();
// set the axes up
var area = _chart.ChartAreas[0];
area.AxisX.IntervalType = DateTimeIntervalType.Number;
area.AxisY.IntervalType = DateTimeIntervalType.Number;
SetChartBoundaries();
// attempt to render the initial chart
if (_seriesIndex.Any())
{
foreach (var series in _seriesIndex)
{
// force both the chart and the internal index to use the same names
series.Value.Name = series.Key;
_chart.Series.Add(series.Value);
}
}
SetChartBoundaries();
}
private void SetChartBoundaries()
{
double maxAxisX, maxAxisY, minAxisX, minAxisY = 0.0d;
var allPoints = _seriesIndex.Values.SelectMany(series => series.Points).ToList();
var yValues = allPoints.SelectMany(point => point.YValues).ToList();
maxAxisX = xPosCounter;
minAxisX = xPosCounter - MaxPoints;
maxAxisY = yValues.Count > 0 ? Math.Ceiling(yValues.Max()) : 1.0d;
minAxisY = yValues.Count > 0 ? Math.Floor(yValues.Min()) : 0.0d;
if (allPoints.Count > 2)
{
var area = _chart.ChartAreas[0];
area.AxisX.Minimum = minAxisX;
area.AxisX.Maximum = maxAxisX;
area.AxisY.Minimum = minAxisY;
area.AxisY.Maximum = maxAxisY;
}
}
private void SetPauseButtonText(bool paused)
{
_pauseButton.Text = $"{(!paused ? "PAUSE ||" : "RESUME ->")}";
}
private void Paused()
{
//while paused, we need to stash AddSeries & RemoveSeries messages
Receive<AddSeries>(addSeries => Stash.Stash());
Receive<RemoveSeries>(removeSeries => Stash.Stash());
Receive<Metric>(metric => HandleMetricsPaused(metric));
Receive<TogglePause>(pause =>
{
SetPauseButtonText(false);
UnbecomeStacked();
//ChartingActor is leaving the Paused state, put message back
//into mailbox for processing under new behavior
Stash.UnstashAll();
});
}
}
}
- ChartingMessage.cs
using Akka.Actor;
namespace Akka_Test.Actors
{
public class GatherMetrics { }
public class Metric
{
public string Series { get; private set; }
public float CounterValue { get; private set; }
public Metric(string series, float counterValue)
{
CounterValue = counterValue;
Series = series;
}
}
public enum CounterType
{
Cpu,
Memory,
Disk
}
public class SubscribeCounter
{
public CounterType Counter { get; private set; }
public IActorRef Subscriber { get; private set; }
public SubscribeCounter(CounterType counter, IActorRef subscriber)
{
Subscriber = subscriber;
Counter = counter;
}
}
public class UnsubscribeCounter
{
public CounterType Counter { get; private set; }
public IActorRef Subscriber { get; private set; }
public UnsubscribeCounter(CounterType counter, IActorRef subscriber)
{
Subscriber = subscriber;
Counter = counter;
}
}
public class ChartingMessages
{
}
}
- PerformanceCounterActor.cs
using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Akka_Test.Actors
{
public class PerformanceCounterActor : UntypedActor
{
private readonly string _seriesName;
private readonly Func<PerformanceCounter> _performanceCounterGenerator;
private PerformanceCounter _counter;
private readonly HashSet<IActorRef> _subscriptions;
private readonly ICancelable _cancelPublishing;
public PerformanceCounterActor(string seriesName, Func<PerformanceCounter> performanceCounterGenerator)
{
_seriesName = seriesName;
_performanceCounterGenerator = performanceCounterGenerator;
_subscriptions = new HashSet<IActorRef>();
_cancelPublishing = new Cancelable(Context.System.Scheduler);
}
protected override void PreStart()
{
//create a new instance of the performance counter
_counter = _performanceCounterGenerator();
Context.System.Scheduler.ScheduleTellRepeatedly(
TimeSpan.FromMilliseconds(250),
TimeSpan.FromMilliseconds(250),
Self,
new GatherMetrics(),
Self,
_cancelPublishing
);
}
protected override void PostStop()
{
try
{
//terminate the scheduled task
_cancelPublishing.Cancel(false);
_counter.Dispose();
}
catch
{
// we don't care about additional "ObjectDisposed" exceptions
}
finally
{
base.PostStop();
}
}
protected override void OnReceive(object message)
{
if(message is GatherMetrics)
{
//publish latest counter value to all subscribers
var metric = new Metric(_seriesName, _counter.NextValue());
foreach (var sub in _subscriptions)
sub.Tell(metric);
}
else if(message is SubscribeCounter)
{
// add a subscription for this counter
var sc = message as SubscribeCounter;
_subscriptions.Add(sc.Subscriber);
}
else if(message is UnsubscribeCounter)
{
//remove a subscription from this counter
var uc = message as UnsubscribeCounter;
_subscriptions.Remove(uc.Subscriber);
}
}
}
}
- PerformanceCounterCoordinatorActor.cs
using Akka.Actor;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms.DataVisualization.Charting;
namespace Akka_Test.Actors
{
public class PerformanceCounterCoordinatorActor : ReceiveActor
{
public class Watch
{
public Watch(CounterType counter)
{
Counter = counter;
}
public CounterType Counter { get; private set; }
}
public class Unwatch
{
public Unwatch(CounterType counter)
{
Counter = counter;
}
public CounterType Counter { get; private set; }
}
private static readonly Dictionary<CounterType, Func<PerformanceCounter>>
CounterGenerators = new Dictionary<CounterType, Func<PerformanceCounter>>()
{
{CounterType.Cpu, () => new PerformanceCounter("Processor",
"% Processor Time", "_Total", true)},
{CounterType.Memory, () => new PerformanceCounter("Memory",
"% Committed Bytes In Use", true)},
{CounterType.Disk, () => new PerformanceCounter("LogicalDisk",
"% Disk Time", "_Total", true)},
};
private static readonly Dictionary<CounterType, Func<Series>> CounterSeries =
new Dictionary<CounterType, Func<Series>>()
{
{CounterType.Cpu, () =>
new Series(CounterType.Cpu.ToString()){
ChartType = SeriesChartType.SplineArea,
Color = Color.DarkGreen}},
{CounterType.Memory, () =>
new Series(CounterType.Memory.ToString()){
ChartType = SeriesChartType.FastLine,
Color = Color.MediumBlue}},
{CounterType.Disk, () =>
new Series(CounterType.Disk.ToString()){
ChartType = SeriesChartType.SplineArea,
Color = Color.DarkRed}},
};
private Dictionary<CounterType, IActorRef> _counterActors;
private IActorRef _chartingActor;
public PerformanceCounterCoordinatorActor(IActorRef chartingActor) :
this(chartingActor, new Dictionary<CounterType, IActorRef>())
{
}
public PerformanceCounterCoordinatorActor(IActorRef chartingActor,
Dictionary<CounterType, IActorRef> counterActors)
{
_chartingActor = chartingActor;
_counterActors = counterActors;
Receive<Watch>(watch =>
{
if (!_counterActors.ContainsKey(watch.Counter))
{
// create a child actor to monitor this counter if
// one doesn't exist already
var counterActor = Context.ActorOf(Props.Create(() =>
new PerformanceCounterActor(watch.Counter.ToString(),
CounterGenerators[watch.Counter])));
// add this counter actor to our index
_counterActors[watch.Counter] = counterActor;
}
// register this series with the ChartingActor
_chartingActor.Tell(new ChartingActor.AddSeries(
CounterSeries[watch.Counter]()));
// tell the counter actor to begin publishing its
// statistics to the _chartingActor
_counterActors[watch.Counter].Tell(new SubscribeCounter(watch.Counter,
_chartingActor));
});
Receive<Unwatch>(unwatch =>
{
if (!_counterActors.ContainsKey(unwatch.Counter))
{
return; // noop
}
// unsubscribe the ChartingActor from receiving any more updates
_counterActors[unwatch.Counter].Tell(new UnsubscribeCounter(
unwatch.Counter, _chartingActor));
// remove this series from the ChartingActor
_chartingActor.Tell(new ChartingActor.RemoveSeries(
unwatch.Counter.ToString()));
});
}
}
}
- App.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="akka" type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" />
</configSections>
<akka>
<hocon>
<![CDATA[
akka {
actor{
deployment{
#used to configure our ChartingActor
/charting{
dispatcher = akka.actor.synchronized-dispatcher #causes ChartingActor to run on the UI thread for WinForms
}
}
}
}
]]>
</hocon>
</akka>
</configuration>
- ChartDataHelper.cs
using Akka.Util;
using System;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;
namespace Akka_Test
{
/// <summary>
/// Helper class for creating random data for chart plots
/// </summary>
public static class ChartDataHelper
{
public static Series RandomSeries(string seriesName, SeriesChartType type = SeriesChartType.Line, int points = 100)
{
var series = new Series(seriesName) { ChartType = type };
foreach (var i in Enumerable.Range(0, points))
{
var rng = ThreadLocalRandom.Current.NextDouble();
series.Points.Add(new DataPoint(i, 2.0 * Math.Sin(rng) + Math.Sin(rng / 4.5)));
}
series.BorderWidth = 3;
return series;
}
}
}
- Form1.cs
using Akka.Actor;
using Akka.Util.Internal;
using Akka_Test.Actors;
using System.Collections.Generic;
using System.Windows.Forms;
namespace Akka_Test
{
public partial class Form1 : Form
{
private IActorRef _chartActor;
private readonly AtomicCounter _seriesCounter = new AtomicCounter(1);
private IActorRef _coordinatorActor;
private Dictionary<CounterType, IActorRef> _toggleActors = new Dictionary<CounterType,
IActorRef>();
public Form1()
{
InitializeComponent();
}
private void Main_Load(object sender, System.EventArgs e)
{
_chartActor = Program.ChartActors.ActorOf(Props.Create(() =>
new ChartingActor(sysChart, btnPauseResume)), "charting");
_chartActor.Tell(new ChartingActor.InitializeChart(null)); //no initial series
_coordinatorActor = Program.ChartActors.ActorOf(Props.Create(() =>
new PerformanceCounterCoordinatorActor(_chartActor)), "counters");
//CPU Button toggle actor
_toggleActors[CounterType.Cpu] = Program.ChartActors.ActorOf(
Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnCpu,
CounterType.Cpu, false))
.WithDispatcher("akka.actor.synchronized-dispatcher"));
//Memory Button toggle actor
_toggleActors[CounterType.Memory] = Program.ChartActors.ActorOf(
Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnMemory,
CounterType.Memory, false))
.WithDispatcher("akka.actor.synchronized-dispatcher"));
//DISK Butto toggle actor
_toggleActors[CounterType.Disk] = Program.ChartActors.ActorOf(
Props.Create(() => new ButtonToggleActor(_coordinatorActor, btnDisk,
CounterType.Disk, false))
.WithDispatcher("akka.actor.synchronized-dispatcher"));
//Set the CPU toggle to ON so we start getting some data
_toggleActors[CounterType.Cpu].Tell(new ButtonToggleActor.Toggle());
}
private void Main_FormClosing(object sender, FormClosingEventArgs e)
{
//shut down the charting actor
_chartActor.Tell(PoisonPill.Instance);
//shut down the ActorSystem
Program.ChartActors.Terminate();
}
private void btnCpu_Click(object sender, System.EventArgs e)
{
_toggleActors[CounterType.Cpu].Tell(new ButtonToggleActor.Toggle());
}
private void btnMemory_Click(object sender, System.EventArgs e)
{
_toggleActors[CounterType.Memory].Tell(new ButtonToggleActor.Toggle());
}
private void btnDisk_Click(object sender, System.EventArgs e)
{
_toggleActors[CounterType.Disk].Tell(new ButtonToggleActor.Toggle());
}
private void btnPauseResume_Click(object sender, System.EventArgs e)
{
_chartActor.Tell(new ChartingActor.TogglePause());
}
}
}
- Form1.designer.cs
namespace Akka_Test
{
partial class Form1
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form 디자이너에서 생성한 코드
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
System.Windows.Forms.DataVisualization.Charting.ChartArea chartArea2 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
System.Windows.Forms.DataVisualization.Charting.Legend legend2 = new System.Windows.Forms.DataVisualization.Charting.Legend();
System.Windows.Forms.DataVisualization.Charting.Series series2 = new System.Windows.Forms.DataVisualization.Charting.Series();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.panel2 = new System.Windows.Forms.Panel();
this.panel1 = new System.Windows.Forms.Panel();
this.sysChart = new System.Windows.Forms.DataVisualization.Charting.Chart();
this.btnCpu = new System.Windows.Forms.Button();
this.btnMemory = new System.Windows.Forms.Button();
this.btnDisk = new System.Windows.Forms.Button();
this.btnPauseResume = new System.Windows.Forms.Button();
this.tableLayoutPanel1.SuspendLayout();
this.panel2.SuspendLayout();
this.panel1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.sysChart)).BeginInit();
this.SuspendLayout();
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.ColumnCount = 2;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 82.625F));
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 17.375F));
this.tableLayoutPanel1.Controls.Add(this.panel2, 1, 0);
this.tableLayoutPanel1.Controls.Add(this.panel1, 0, 0);
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.RowCount = 1;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(800, 450);
this.tableLayoutPanel1.TabIndex = 0;
//
// panel2
//
this.panel2.Controls.Add(this.btnPauseResume);
this.panel2.Controls.Add(this.btnDisk);
this.panel2.Controls.Add(this.btnMemory);
this.panel2.Controls.Add(this.btnCpu);
this.panel2.Dock = System.Windows.Forms.DockStyle.Fill;
this.panel2.Location = new System.Drawing.Point(664, 3);
this.panel2.Name = "panel2";
this.panel2.Size = new System.Drawing.Size(133, 444);
this.panel2.TabIndex = 1;
//
// panel1
//
this.panel1.Controls.Add(this.sysChart);
this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.panel1.Location = new System.Drawing.Point(3, 3);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(655, 444);
this.panel1.TabIndex = 0;
//
// sysChart
//
chartArea2.Name = "ChartArea1";
this.sysChart.ChartAreas.Add(chartArea2);
this.sysChart.Dock = System.Windows.Forms.DockStyle.Fill;
legend2.Name = "Legend1";
this.sysChart.Legends.Add(legend2);
this.sysChart.Location = new System.Drawing.Point(0, 0);
this.sysChart.Name = "sysChart";
series2.ChartArea = "ChartArea1";
series2.Legend = "Legend1";
series2.Name = "Series1";
this.sysChart.Series.Add(series2);
this.sysChart.Size = new System.Drawing.Size(655, 444);
this.sysChart.TabIndex = 0;
this.sysChart.Text = "chart1";
//
// btnCpu
//
this.btnCpu.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this.btnCpu.Location = new System.Drawing.Point(2, 259);
this.btnCpu.Name = "btnCpu";
this.btnCpu.Size = new System.Drawing.Size(130, 54);
this.btnCpu.TabIndex = 0;
this.btnCpu.Text = "CPU (ON)";
this.btnCpu.UseVisualStyleBackColor = true;
this.btnCpu.Click += new System.EventHandler(this.btnCpu_Click);
//
// btnMemory
//
this.btnMemory.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this.btnMemory.Location = new System.Drawing.Point(2, 319);
this.btnMemory.Name = "btnMemory";
this.btnMemory.Size = new System.Drawing.Size(130, 54);
this.btnMemory.TabIndex = 1;
this.btnMemory.Text = "MEMORY (OFF)";
this.btnMemory.UseVisualStyleBackColor = true;
this.btnMemory.Click += new System.EventHandler(this.btnMemory_Click);
//
// btnDisk
//
this.btnDisk.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this.btnDisk.Location = new System.Drawing.Point(3, 379);
this.btnDisk.Name = "btnDisk";
this.btnDisk.Size = new System.Drawing.Size(130, 54);
this.btnDisk.TabIndex = 2;
this.btnDisk.Text = "DISK (OFF)";
this.btnDisk.UseVisualStyleBackColor = true;
this.btnDisk.Click += new System.EventHandler(this.btnDisk_Click);
//
// btnPauseResume
//
this.btnPauseResume.Font = new System.Drawing.Font("굴림", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
this.btnPauseResume.Location = new System.Drawing.Point(2, 144);
this.btnPauseResume.Name = "btnPauseResume";
this.btnPauseResume.Size = new System.Drawing.Size(130, 54);
this.btnPauseResume.TabIndex = 3;
this.btnPauseResume.Text = "PAUSE ||";
this.btnPauseResume.UseVisualStyleBackColor = true;
this.btnPauseResume.Click += new System.EventHandler(this.btnPauseResume_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Controls.Add(this.tableLayoutPanel1);
this.Name = "Form1";
this.Text = "Form1";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Main_FormClosing);
this.Load += new System.EventHandler(this.Main_Load);
this.tableLayoutPanel1.ResumeLayout(false);
this.panel2.ResumeLayout(false);
this.panel1.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.sysChart)).EndInit();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.Panel panel2;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.DataVisualization.Charting.Chart sysChart;
private System.Windows.Forms.Button btnDisk;
private System.Windows.Forms.Button btnMemory;
private System.Windows.Forms.Button btnCpu;
private System.Windows.Forms.Button btnPauseResume;
}
}
- Program.cs
using Akka.Actor;
using System;
using System.Windows.Forms;
namespace Akka_Test
{
static class Program
{
/// <summary>
/// ActorSystem we'll be using to publish data to charts
/// and subscribe from performance counters
/// </summary>
public static ActorSystem ChartActors;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
ChartActors = ActorSystem.Create("ChartActors");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}
- 실행 결과
728x90
'C# > Akka(Actor)' 카테고리의 다른 글
[C#] Akka 중급 2-4 | 'BecomeStacked' 와 'UnbecomeStacked' 를 사용하여 런타임에 액터 동작 전환 (0) | 2022.06.16 |
---|---|
[C#] Akka 중급 2-3 | 'Scheduler' 를 사용한 지연 메시지 보내기 (0) | 2022.06.15 |
[C#] Akka 중급 2-2 | 더 나은 메시지 처리를 위해 'ReceiveActor' 사용 (0) | 2022.06.15 |
[Akka.NET] Akka.NET Helper 정의 (0) | 2022.01.17 |
[Akka.NET]Akka.Remote 란? (0) | 2022.01.17 |
이 글을 공유하기