[C#] Akka 중급 2-4 | 'BecomeStacked' 와 'UnbecomeStacked' 를 사용하여 런타임에 액터 동작 전환

참조

소개

  • BecomeStacked와 UnbecomeStacked를 사용하여 런타임에 액터 동작 전한하는 방법에 대해서 알아봅니다.

실제 시나리오 : 인증

  • Akka.NET 액터를 사용해 간단한 채팅 시스템을 구축한다고 가정해 보겠습니다.
  • 특정 사용자와의 모든 통신을 담당하는 액터가 당신이 원하는 UserActor의 모습입니다.
public class UserActor : ReceiveActor 
{
    private readonly string _userId;
    private readonly string _charRoomId;

    public UserActor(string userId, string chatRoomId)
    {
        _userId = userId;
        _charRoomId = chatRoomId;
        Receive<IncomingMessage>(inc => inc.ChatRoomId == _chatRoomId,
                    inc => {
                        // print message for user
                    });

        Receive<OutgoingMessage>(int => inc.ChatRoomId == _chatRoomId,
                    int => {
                        // send message to chatroom
                    });
    }
}
  • 아래와 같이 같은 타입의 채팅 메시지를 다르게 처리할 수 있도록 액터를 다시 작성할 수 있을까요?
    • 인증 중인 사용자
    • 인증된 사용자이거나
    • 인증할 수 없는 사용자?
  • 간단합니다. 액터의 전환 가능 동작(switchable behavior) 을 이용해 할 수 있습니다.

전환 가능 동작(switchable behavior)이 무엇인가요?

  • Actor Model 에서 액터의 핵심 속성 중 하나는 액터가 처리하는 메시지 간에 행동을 변경할 수 있다는 것입니다.
  • 이 기능을 사용하면, 유한 상태 머신(Finite-state machine) 빌드나 액터가 수신한 메시지에 따라 메시지를 처리하는 방식을 변경하는 것과 같은 모든 종류의 작업을 수행할 수 있습니다.
  • 전환 가능 동작은 진정한 액터 시스템의 가장 강력하고 기본적인 기능 중 하나입니다.
  • 액터의 재사용을 가능하게 하는 주요 기능 중 하나이며, 매우 작은 코드 풋프린트로 엄청난 양의 작업을 수행할 수 있도록 도와줍니다.

동작 스택(Behavior Stack)

  • Akka.NET 액터는 "동작스택" 이라는 개념을 가지고 있습니다.
  • 동작 스택의 맨 위에 있는 방법은 액터의 현재 동작을 정의합니다.
  • 현재 동작은 Authenticating() 입니다.

Become과 BecomeStacked 를 사용하여 새로운 동작을 채택

  • BecomeStacked를 호출할 때마다 ReceiveActor에게 새로운 동작을 스택에 푸시하도록 지시합니다.
  • 이 새로운 동작은 액터에게 전달된 메시지를 처리하는 데 사용할 Receive 메서드를 지정합니다.
  • 예제 액터가 BecomeStacked를 통해 Authenticated가 될 때 동작 스택에 일어나는 일입니다.

NOTE : Become은 스택에서 이전 동작을 삭제하므로 스택에는 한 번에 하나 이상의 동작이 포함되지 않습니다.

동작을 스택에 푸시하려면 BecomeStacked를 사용하고 이전 동작으로 되돌리려면 UnbecomeStacked를 사용합니다. 사용자는 대부분 Become만 사용하면 됩니다.

UnbecomeStacked를 사용하여 이전 동작으로 되돌리기

  • 액터가 동작 스택의 이전 동작으로 되돌아 가도록 하려면 UnbecomeStacked를 호출하기만 하면 됩니다.
  • UnbecomeStacked를 호출할 때마다 스택에서 현재 동작을 꺼내서 이전 동작으로 대체합니다.
  • 다시 말하지만 이 새로운 동작은 들어오는 메시지를 처리하는 데 사용되는 Receive 메서드를 말합니다.
  • 다음은 예제 액터 UnbecomeStacked 가 작동할 때 동작 스택에 일어나는 일입니다.

동작을 변경하는 API는 무엇입니까?

  • 동작을 변경하는 API는 정말 간답합니다.

    • Become - 현재 수신 루프를 지정된 루프로 바꿉ㄴ디ㅏ. 동작 스택을 제거합니다.
    • BecomeStacked - 지정된 메서드를 동작 스택의 맨 위에 추가하고 그 아래의 이전 메서드를 유지합니다.
    • UnBecomeStacked - 스택에서 이전에 수신한 메서드로 되돌립니다. (BecomeStacked 에서만 작동)
  • BecomeStacked 는 이전 동작을 보존하므로 UnbecomeStacked를 호출하여 이전 동작으로 돌아갈 수 있다는 것이 차이점입니다.

  • 다른 무엇보다 당신의 필요에 달려 있습니다.

  • 필요한 만큼 BecomeStacked를 호출할 수 있으며, BecomeStacked를 호출한 횟수만큼 UnbecomeStacked를 호출할 수 있습니다.

  • 현재 동작이 스택의 유일한 동작인 경우 UnbecomeStacked에 대한 추가 호출은 아무 작업도 수행하지 않습니다.

액터가 행동을 바꾸는 것이 문제가 되지 않나요?

  • 아니요. 실제로 안전하며 ActorSystem에 엄청난 유연성과 코드 재사용을 제공하는 기능입니다.
  • 다음은 전환 가능 동작에 대한 몇 가지 일반적이 질문입니다.

새로운 동작은 언제 적용되나요?

  • Akka.NET 액터는 한 번에 하나의 메시지만 처리하므로 액터 메시지 처리 동작을 안전하게 전환할 수 있습니다.
  • 새 메시지 처리 동작은 다음 메시지가 도착할 때까지 적용되지 않습니다.

Become이 동작 스택을 날려 버리는 것이 나쁘지 않나요?

  • 지금까지 가장 일반적으로 사용되는 방식입니다.
  • 한 동작에서 다른 동작으로 명시적으로 전환하는 것이 동작 전황에 사용되는 가장 일반적인 방식입니다.
  • 간단하고 명시적인 스위치를 사용하면 코드를 훨씬 쉽게 읽고 추론할 수 있습니다.
  • 실제로 동작 스택을 활용해야 하는 경우 - 단순하고, 명시적인 become 이 상황에 맞지 않는 경우, 동작스택을 사용할 수 있습니다.
  • 이 레슨에서는 BecomeStacked와 UnbecomeStacked를 사용하여 시연합니다. 보통 우리는 그냥 Becom을 사용합니다.

동작 스택의 깊이는 얼마나 되나요?

  • 스택은 정말 깊을 수 있지만, 무제한은 아닙니다.
  • 또한 액터가 다시 시작될 때마다 동작 스택이 지워지고 사용자가 코드화한 초기 동작부터 액터가 시작됩니다.

UnbecomeStacked를 호출하고 동작 스택에 아무것도 남지 않으면 어떻게 되나요?

  • UnbecomeStacked는 안전한 방법이며 현재 동작이 스택의 유일한 동작인 경우 아무 작업도 수행하지 않습니다.

실제 사례로 돌아가기

  • 이제 전환 가능 동작을 이해했으므로, 실제 시나리오로 돌아가서 어떻게 사용되는지 살펴보겠습니다.

  • 채팅 시스템 액터에 인증을 추가해야 합니다.

  • 따라서 다음과 같은 경우 채팅 메시지를 다르게 처리하도록 이 액터를 다시 작성할 수 있나요?

    • 인증 중인 사용자
    • 인증된 사용자이거나
    • 인증할 수 없는 사용자?
  • 다음은 기본 인증을 처리하기 위해 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>(inc => inc.ChatRoomId == _chatRoomId,
            inc => {
                //have to reject message - auth failed.
            });

        Receive<OutgoingMessage>(inc => 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
            });
    }
}
  • 먼저 ReceiveActor 에 정의된 Receive<T> 핸들러를 가져와서 세가지 별도의 메서드로 옮겼습니다.
  • 이러한 각 메서드는 메시지를 처리하는 방법을 제어하는 상태를 나타냅니다.
    • Authenticating() : 이 동작은 사용자가 인증을 시도 할 때 메시지를 처리하는 데 사용됩니다.
    • Authenticated() : 이 동작은 인증 작업이 성공할 때 메시지를 처리하는 데 사용됩니다.
    • Unauthenticated() : 이 동작은 인증 작업이 실패 할 때 메시지를 처리하는데 사용됩니다.
  • 생성자에서 Authenticating()을 호출했으므로, 액터는 Authenticating() 상태에서 시작했습니다.
  • 즉, Authenticating() 메서드에 정의된 Receive<T> 핸들러만 메시지 처리에 사용됩니다.
  • 그러나, AuthenticationSuccess 또는 AuthenticationFailure 유형의 메시지를 수신하면 Become 메서드를 사용하여 동작을 각각 Authenticated 또는 Unauthenticated로 전환합니다.

UntypedActor 에서 동작을 전환 할 수 있나요?

  • UntypedActor 에서 동작을 전환하려면 직접 호출하는 대신 ActorContext를 통해 BecomeStacked 와 UnbecomeStacked에 엑세스 해야 합니다.
  • UntypedActor 내부의 API 호출입니다.
    • Context.Become - 스택의 이전 동작을 보존하지 않고 동작을 변경합니다.
    • Context.BecomeStacked - 스택에 새로운 동작을 푸시하거나
    • Context.UnbecomeStacked() - 현재 동작을 팝하고 이전 동작으로 전환합니다.
  • Context.Become 의 첫 번째 인수는 Receive 대리자로, 다음 서명을 가진 모든 메서드입니다.
void MethodName(object someParameterName);
  • 이 대리자(delegate)는 메시지를 수신하고 새 동작 상태를 나타내는 액터의 다른 메서드를 나타내는 데만 사용됩니다.
  • 다음은 예입니다. (OtherBehavior 는 Receive 대리자입니다.)
public class MyActor : UntypedActor
{
    protected override void OnReceive(object message)
    {
        if(message is SwitchMe)
        {
            //preserve the previous behavior on the stack
            Context.BecomeStacked(OtherBehavior);
        }
    }

    //OtherBehavior is a Receive delegate
    private void OtherBehavior(object message)
    {
        if(message is SwitchMeBack)
        {
            //switch back to previous behavior on the 
            Context.UnbecomeStacked();
        }
    }
}
  • 이러한 구문상의 차이를 제외하고, 동작 전환은 UntypedActor와 ReceiveActor 모두에게 정확히 동일한 방식으로 작동합니다.

실습

  • 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
    {
        public const int MaxPoints = 250;
        public int xPosCounter = 0;

        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()
        {
            Receive<Metric>(metric => HandleMetricsPaused(metric));
            Receive<TogglePause>(pause =>
            {
                SetPauseButtonText(false);
                UnbecomeStacked();
            });
        }
    }
}
  • 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
{
///


/// Helper class for creating random data for chart plots
///

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**

```csharp
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

이 글을 공유하기

댓글

Designed by JB FACTORY