[C#] Akka 중급 2-2 | 더 나은 메시지 처리를 위해 'ReceiveActor' 사용

참조

소개

  • Unit1 에서는 UntypedActor를 사용하여 첫 번째 액터를 빌드하고 몇 가지 간단한 메시지 유형을 처리하는 방법을 배웠습니다.
  • 이번 레슨에서는 ReceiveActor를 사용하여 Akka.NET에서 보다 정교한 유형의 패턴 일치 및 메시지 처리를 쉽게하는 방법을 보여줄 것입니다.

Key Concepts / Background

패턴매칭(Pattern matching)

  • Akka.NET의 액터는 .NET 타입이나 값에 따라 메시지를 선택적으로 처리할 수 있는 패턴 일치의 개념에 크게 의존합니다.
  • 첫 번째 모듈에서는 UntypedActor를 사용하여 다음과 같은 코드 블록을 사용하여 메시지를 처리하고 수신하는 방법을 배웠습니다.
protected override void OnReceive(object message)
{
    if(message is Foo)
    {
        var foo = message as Foo;
        //do something with foo
    }
    else if(message is Bar)
    {
        var bar = message as Bar;
        //do something with bar
    }
    else
    {
        //couldn't match this message
        Unhandled(message);
    }
}
  • Akka.NET 에서 이 패턴 일치 방법은 단순 일치에 적합하지만 일치 요구가 더 복잡하다면 어떻게 해야 할까요?
  • 지금까지 살펴본 UntypedActor로 이러한 사용 사례를 어떻게 처리할지 고려하십시오.
    1. message가 string 이고 "AkkaDotNet" 으로 시작하는 경우 또는
    2. message가 Foo 타입이고 Foo.Count가 4보다 작고 Foo.Max가 10보다 큰 경우
protected override void OnReceive(object message)
{
    if(message is string && message.AsInstanceOf<string>().BeginsWith("AkkaDotNet"))
    {
        var str = message as string;
    }
    else if(message is Foo && message.AsInstanceOf<Foo>.Count < 4 && 
                              message.AsInstanceOf<Foo>.Count > 10) 
    {
        var foo = message as Foo;
    }
    else
    {
        Unhandled(message);
    }
}
  • UntypedActor를 이용하여 패턴매칭을 하니까 상당히 소스코드가 복잡해 집니다.
  • 그러나, ReceiveActor를 이용하면 보다 간단하게 코드 구현이 가능합니다.
public class FooActor : ReceiveActor
{
    public FooActor
    {
        Receive<string>(s => s.StartWith("AkkaDotNet"), s =>
        {
            //handle string
        });

        Receive<Foo>(foo => foo.Count < 4 && foo.Max > 10, foo => {
            //handle foo
        });
    }
}

ReceiveActor의 비밀 소스

  • 패턴 일치 코드를 모두 정리하는 비밀 소스는 Receive<T> 핸들러입니다.
// ReceiveActor를 파워풀하게 만드는 이유입니다.
Receive<T>(Predicate<T>, Action<T>);
  • ReceiveActor를 사용하면 액터에 맞는 강력한 타입의 컴파일 타임 패턴 레이어를 쉽게 추가 할 수 있습니다.
  • 타입에 따라 메시지를 쉽게 일치시킨 다음, 액터가 특정 메시지를 처리 할 수 있는지 여부를 결정할 때 추가 검사 또는 유효성 검사를 수행하기 위해 유형화된 술어를 사용할 수 있습니다.

다른 종류의 Receive<T> 핸들러가 있나요?

1. Receive<T>(Action<T> handler)

  • 메시지가 'T' 타입인 경우에만 메시지 핸들러를 실행합니다.

2. Receive<T>(Predicate<T> pred, Action<T> handler)

  • 이것은 메시지가 T 유형이고 predicate 함수가 T 인스턴스에 대해 true를 반환하는 경우에만 메시지 핸들러를 실행합니다.

3. Receive<T>(Action<T> handler, Predicate<T> pred)

  • 이전과 같습니다.

4. Receive(Type type, Action<object> handler)

  • 이것은 이전의 typed + predicate 메시지 핸들러의 구체적인 버전입니다.(더 이상 일반이 아님)

5. Receive(Type type, Action<object> handler, Predicate<object> pred)

  • 이전과 같습니다.

6. ReceiveAny()

  • 이것은 모든 object 인스턴스를 받아들이는 catch-all 핸들러입니다.
  • 일반적으로 이 기능은 이전 Receive() 처리기에서 처리되지 않은 메시지를 처리하는 데 사용합니다.

Receive<T> 핸들러를 선언하는 순서가 중요합니다.

  • 겹치는 메시지 유형을 처리해야하는 경우 어떻게 됩니까?

  • 아래 메시지를 고려하십시오. 동일한 하위 문자열로 시작하지만 다르게 처리해야 한다고 가정합니다.

    1. AkkaDotNetSuccess 로 시작하는 string 메시지, 그리고
    2. AkkaDotNet으로 시작하는 string 메시지
  • ReceiveActor가 이렇게 작성되면 어떻게 될까요?

public class StringActor : ReceiveActor
{
    public StringActor()
    {
        Receive<string>(s => s.StartWith("AkkaDotNet"), s => {
            //handle string
        });

        Receive<string>(s => s.StartWith("AkkaDotNetSuccess"), s => {
            //handle string
        });
    }
}
  • 이 경우에 일어나는 일은 두 번째 핸들러(for s.StartWith("AkkaDotNetSuccess")) 가 호출되지 않는다는 것입니다.
  • Receive<T> 핸들러의 순서가 중요합니다.
  • 이는 ReceiveActor가 최상위 일치 핸들러가 아닌 첫 번째 일치 핸들러를 사용하여 메시지를 처리하고 선언 된 순서대로 각 메시지에 대한 핸들러를 평가하기 떄문입니다.
  • 그렇다면 "AkkaDotNetSuccess" 로 시작하는 문자열에 대한 핸들러가 트리거되지 않는 위의 문제를 어떻게 해결합니까?
  • 간단합니다. 더 구체적인 핸들러가 먼저 나오도록 하면 이 문제를 해결 할 수 있습니다.
public class StringActor : ReceiveActor
{
    public StringActor()
    {
        //Now works as expected
        Receive<string>(s => s.StartWith("AkkaDotNetSuccess"), s => {
            //handle string
        });

        Receive<string>(s => s.StartWith("AkkaDotNet"), s =>
        {
            //handle string
        });
    }
}

ReceiveActor 에서 메시지 핸들러는 어디에서 정의합니까?

  • ReceiveActor에는 OnReceive() 메서드가 없습니다.
  • 대신, ReceiveActor 생성자 또는 해당 생성자가 호출 한 메서드에서 직접 Receive 메시지 핸들러를 연결해야 합니다.
  • 이것을 알면 ReceiveActor를 사용하여 작업할 수 있습니다.

실습

  • ChartingActor.cs
using Akka.Actor;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms.DataVisualization.Charting;

namespace Akka_Test.Actors
{
    public class ChartingActor : ReceiveActor
    {
        public class InitializeChart
        {
            public Dictionary<string, Series> InitialSeries { get; private set; }
            public InitializeChart(Dictionary<string, Series> initialSeries)
            {
                InitialSeries = initialSeries;
            }
        }

        public class AddSeries
        {
            public Series Series { get; private set; }
            public AddSeries(Series series)
            {
                Series = series;
            }
        }

        private readonly Chart _chart;
        private Dictionary<string, Series> _seriesIndex;
        public ChartingActor(Chart chart) : this(chart, new Dictionary<string, Series>())
        {

        }

        public ChartingActor(Chart chart, Dictionary<string, Series> seriesIndex)
        {
            _chart = chart;
            _seriesIndex = seriesIndex;

            Receive<InitializeChart>(ic => HandleInitialize(ic));
            Receive<AddSeries>(addSeries => HandleAddSeries(addSeries));
        }

        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);
            }
        }

        private void HandleInitialize(InitializeChart ic)
        {
            if(ic.InitialSeries != null)
            {
                _seriesIndex = ic.InitialSeries;
            }

            _chart.Series.Clear();

            if(_seriesIndex.Any())
            {
                foreach(var series in _seriesIndex)
                {
                    series.Value.Name = series.Key;
                    _chart.Series.Add(series.Value);
                }
            }
        }
    }
}
  • 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;
using System.Windows.Forms.DataVisualization.Charting;

namespace Akka_Test
{
    public partial class Form1 : Form
    {
        private IActorRef _chartActor;
        private readonly AtomicCounter _seriesCounter = new AtomicCounter(1);

        public Form1()
        {
            InitializeComponent();
        }

        private void Main_Load(object sender, System.EventArgs e)
        {
            _chartActor = Program.ChartActors.ActorOf(Props.Create(() => new ChartingActor(sysChart)), "charting");
            var series = ChartDataHelper.RandomSeries("FakeSeries" + _seriesCounter.GetAndIncrement());
            _chartActor.Tell(new ChartingActor.InitializeChart(new Dictionary<string, Series>()
            {
                {series.Name, series}
            }));
        }

        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 uiBtn_AddSeries_Click(object sender, System.EventArgs e)
        {
            var series = ChartDataHelper.RandomSeries($"FakeSeries {_seriesCounter.GetAndIncrement()}");
            _chartActor.Tell(new ChartingActor.AddSeries(series));
        }
    }
}
  • 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.panel1 = new System.Windows.Forms.Panel();
            this.panel2 = new System.Windows.Forms.Panel();
            this.sysChart = new System.Windows.Forms.DataVisualization.Charting.Chart();
            this.uiBtn_AddSeries = new System.Windows.Forms.Button();
            this.tableLayoutPanel1.SuspendLayout();
            this.panel1.SuspendLayout();
            this.panel2.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;
            // 
            // 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;
            // 
            // panel2
            // 
            this.panel2.Controls.Add(this.uiBtn_AddSeries);
            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;
            // 
            // 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";
            // 
            // uiBtn_AddSeries
            // 
            this.uiBtn_AddSeries.BackColor = System.Drawing.Color.White;
            this.uiBtn_AddSeries.Font = new System.Drawing.Font("굴림", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(129)));
            this.uiBtn_AddSeries.Location = new System.Drawing.Point(20, 9);
            this.uiBtn_AddSeries.Name = "uiBtn_AddSeries";
            this.uiBtn_AddSeries.Size = new System.Drawing.Size(104, 45);
            this.uiBtn_AddSeries.TabIndex = 0;
            this.uiBtn_AddSeries.Text = "Add Series";
            this.uiBtn_AddSeries.UseVisualStyleBackColor = false;
            this.uiBtn_AddSeries.Click += new System.EventHandler(this.uiBtn_AddSeries_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.panel1.ResumeLayout(false);
            this.panel2.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 uiBtn_AddSeries;
    }
}
  • 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