[C#] Akka 중급 2-1 | HOCON Configuration을 사용하여 Akka.NET 구성

참조

소개

  • Unit2 에서 대부분의 시간을 차트의 모든 데이터를 실제로 플로팅하는 역할을 담당하는 액터인 ChartingActor와 함께 작업할 것입니다.

Key Concepts / Background

Dispatcher 란?

  • Dispatcher는 액터의 Mailbox에서 액터 인스턴스 자체로 메시지를 푸시하는 스택의 일종입니다.
  • 즉, Dispatcher는 액터의 OnReceive() 메서드로 메시지를 푸시하는 것입니다.
  • 주어진 Dispatcher를 공유하는 모든 액터는 병렬 실행을 위해 Dispatcher의 스레드도 공유합니다.
  • Akka.NET의 기본 디스패처는 ThreadPoolDispatcher 입니다.
  • 짐작할 수 있듯이, 이 디스패처는 CLR ThreadPool을 기반으로 모든 액터를 실행합니다.

어떤 종류의 Dispatcher 가 있나요?

  • 액터와 함께 사용할 수 있는 여러 유형의 Dispatcher 가 있습니다.

SingleThreadDispatcher

  • 이 Dispatcher는 단일 스레드에서 여러 액터를 실행합니다.

ThreadPoolDispatcher(default)

  • 이 Dispatcher는 최대 동시성을 위해 CLR ThreadPool 위에서 액터를 실행합니다.

SynchronizedDispatcher

  • 이 Dispatcher는 모든 액터 메시지가 호출자(Sender)와 동일한 동기화 컨텍스트에서 처리되도록 예약합니다.
  • 99%의 경우 여기에서 클라이언트 애플리케이션과 같이 UI 스레드에서 액세스해야 하는 액터를 실행합니다.
  • SynchronizedDispatcher는 현재 SynchronizationContext를 사용하여 실행을 예약합니다.

Note: 일반적으로 SynchronizedDispatcher에서 실행되는 액터는 많은 작업을 수행하지 않아야 합니다. 다른 풀에서 실행중인 액터가 수행 할 수 있는 추가 작업을 수행하지 마십시오.

  • 이 레슨에서는 SynchronizedDispatcher 를 사용하여 ChartingActor가 WinForms 애플리케이션의 UI 스레드에서 실행되도록 할 것입니다.
  • 이렇게하면 ChartingActor는 크로스 스레드 마샬링 없이 원하는 모든 UI 요소를 업데이트 할 수 있습니다.
  • 액터의 Dispatcher 가 자동으로 처리할 수 있습니다.

ForkJoinDispatcher

  • 이 Dispatcher는 조정 가능한 동시성을 위해 전용 스레드 그룹에서 액터를 실행합니다.
  • 실행을 위해 전용 스레드가 필요한 액터(격리 보장이 필요한)를 위한 것입니다.
  • 이것은 주로 System 액터가 사용하므로 많이 건드리지 않을 것입니다.

UI 스레드에서 액터를 실행하는 것이 나쁜 생각인가요?

  • UI 스레드에서 액터를 실행하는 것은 해당 액터가 디스크 또는 네트워크 I/O 와 같은 장기 실행 작업을 수행하지 않는 한 괜찮습니다.
  • 사실 UI 스레드에서 액터를 실행하는 것은 UI 이벤트 및 업데이트를 처리하는데 현명한 작업입니다.
  • UI 스레드에서 액터를 실행하면 일반적인 동기화 문제가 모두 제거 되는데, 그렇지 않으면 다중 스레드 WPF 또는 WinForms 앱에서 해야 할 일이 있습니다.

기억하세요:Akka.NET 액터는 게으르다. 메시지를 받지 않을 때는 아무 작업도 하지 않습니다. 비활성 상태에서 리소스를 소비하지 않습니다.

Dispatcher는 깨진 차트와 어떤 관련이 있나요?

  • 앞에서 본 것처럼 그래프를 수행하는 액터(ChartingActor) 가 이벤트를 UI 스레드와 동기화하지 않기 때문에 차트가 업데이트 되지 않습니다.
  • 문제를 해결하기 위해 우리가 해야 할 일은 ChartingActor를 CurrentSynchronizationContextDispatcher 를 사용하도록 변경하는 것 뿐입니다. 그러면 UI 스레드에서 자동으로 실행됩니다.
  • 그러나 우리는 실제 액터 코드를 건드리지 않고 이것을 하고 싶습니다. 액터 자체를 수정하지 않고 CurrentSynchronizationContextDispatcher 를 사용하도록 ChartingActor를 배포하려면 HOCON을 사용하면 됩니다.

HOCON

  • Akka.NET은 HOCON이라는 구성 형식을 활용하여, 원하는 세분화 수준으로 Akka.NET 응용 프로그램을 구성할 수 있습니다.

HOCON 이란?

  • HOCON(Human-Optimized Config Object Notation) 은 유연하고 확장 가능한 구성 형식입니다.
  • 이를 통해 Akka.NET의 IActorRefProvider를 구현, 로깅, 네트워크 전송, 그리고 더 일반적으로 개별 액터가 배포되는 방식에서 모든 것을 구성 할 수 있습니다.
  • HOCON에서 반환하는 값은 유형이 강력합니다.(int, Timespan 등을 가져올 수 있습니다.)

HOCON 으로 무엇을 할 수 있나요?

  • HOCON은 App.config와 Web.config의 읽기 어려운 XML 내에 쉽게 읽을 수 있는 구성을 포함할 수 있습니다.
  • HOCON은 섹션 경로 구성을 쿼리할 수 있으며 해당 섹션은 애플리케이션 내에서 사용할 수 있는 강력한 형식과 구문 분석된 값을 노출합니다.
  • HOCON은 구성 섹션을 중첩하거나 아니면 연결하여 세분화 계층을 생성하고 의미상 네임스페이스 구성을 제공 할 수 있습니다.

HOCON은 일반적으로 어디에 사용되나요?

  • HOCON은 일반적으로 로깅 설정을 조정하고, 특수 모듈을 활성화 하거나, 이 레슨에서 ChartingActor에 대한 Dispatcher와 같은 배포를 구성하는데 사용됩니다.
  • 예를 들어 HOCON으로 ActorSystem을 구성 해 보겠습니다.
var config = ConfigurationFactory.ParseString(@"
akka.remote.helios.tcp {
    transport-class = ""Akka.Remote.Transport.HeliosTcpTransport, Akka.Remote""
    transport-protocol = tcp
    port = 8091
    hostname = ""127.0.0.1""
}");

var system = ActorSystem.Create("MyActorSystem", config);
  • 예제에서 볼 수 있듯이, ConfigurationFactory.ParseString 메서드를 사용하여 string 에서 HOCON Config 객체를 구문 분석 할 수 있습니다.
  • 일단 Config 객체가 있으면 ActorSystem.Create 메서드 내의 ActorSystem에 이를 전달할 수 있습니다.

NOTE: 이 예에서 우리는 Unit2 에서 다루는 개념을 훨씬 뛰어 넘는 개념인 Akka.Remote 와 함께 사용할 특정 네트워크 전송을 구성했습니다. 지금은 세부 사항에 대해 걱정하지 마십시오.

배포(Deployment)? 그건 뭔가요?

  • 배포(Deployment) 는 모호한 개념이지만 HOCON과 밀접한 관련이 있습니다.
  • 액터는 인스턴스화되어 ActorSystem 어딘가에 서비스될 때 배포가 됩니다.
  • 액터가 ActorSystem 내에서 인스턴스화되면 로컬 프로세스 내부 또는 다른 프로세스의 두 위치 중 하나에 배치 될 수 있습니다.
  • 액터가 ActorSystem에 의해 배포되면 다양한 구성 설정이 있습니다.
  • 이 설정은 액터에 대한 광범위한 동작 옵션을 제어합니다. 예를 들면 다음과 같습니다.
  • 이 액터가 라우터가 될까요? 어떤 Dispatcher를 사용합니까? 어떤 유형의 Mailbox가 있습니까?
  • 모든 옵션의 의미를 살펴 보지는 않았지만 지금 알아야 할 핵심 사항은 액터를 서비스에 배포하기 위해 ActorSystem이 사용하는 설정을 HOCON 내에서 설정할 수 있다는 것입니다.
  • 이것은 실제로 액터 코드 자체를 건드리지 않고도 액터의 동작을 극적으로 변경할 수 있음을 의미합니다.

HOCON은 App.config와 Web.config 내에서 사용할 수 있습니다.

NOTE: App.config와 Web.config는 .NET Core에서 더이상 사용되지 않는다는 것을 알고 있습니다. 독립 실행형 HOCON 3.0이 출시되면 HOCON 구성을 선언하는 새로운 방법으로 이 샘플을 업데이트 할 것입니다.

  • string 에서 HOCON을 구문 분석하는 것은 작은 구성 섹션에 편리하지만 App.config 와 Web.config 에 대한 구성 변환 및 기타 모든 유용한 도구를 활용하려면 어떻게 해야 합니까? System.Configuration 네임스페이스에 있습니까?
  • 결과적으로 이러한 구성 파일 내에서도 HOCON을 사용할 수 있습니다.
  • 다음은 App.config 내에서 HOCON을 사용하는 예제입니다.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>

   <configSections>
      <section name="akka"
             type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka"/>
   </configSections>

   <akka>
      <hocon>
         <![CDATA[
            akka {
               # here we are configuring log levels
               log-config-on-start = off
               stdout-loglevel = INFO
               loglevel = ERROR
               # this config section will be referenced as akka.actor
               actor {
                  provider = "Akka.Remote.RemoteActorRefProvider, Akka.Remote"
                  debug {
                     receive = on
                     autoreceive = on
                     lifecycle = on
                     event-stream = on
                     unhandled = on
                  }
               }

               # here we're configuring the Akka.Remote module
               remote {
                  helios.tcp {
                     transport-class =
                  "Akka.Remote.Transport.Helios.HeliosTcpTransport, Akka.Remote"
                     #applied-adapters = []
                     transport-protocol = tcp
                     port = 8091
                     hostname = "127.0.0.1"
                     }
                  log-remote-lifecycle-events = INFO
               }
         ]]>
      </hocon>
   </akka>
</configuration>

HOCON Configuration은 폴백을 지원합니다.

  • Unit2 에서 명시적으로 활용하는 개념은 아니지만 많은 프로덕션 사용 사례에서 유용하게 사용되는 Config 클래스의 강력한 특성입니다.
  • HOCON은 "대체(fallback)" 구성 개념을 지원합니다. 이 개념을 시각적으로 설명하는 것이 가장 쉽습니다.

  • 위의 다이어그램과 같은 것을 만들려면 다음과 같은 구문을 사용하여 세 개의 폴백이 연결된 Config 객체를 만들어야 합니다.
var f0 = configurationFactory.ParseString("a = bar");
var f1 = configurationFactory.ParseString("b = biz");
var f2 = configurationFactory.ParseString("c = baz");
var f3 = configurationFactory.ParseString("a = foo");

var yourConfig = f0.WithFallback(f1)
                                    .WithFallback(f2)
                                    .WithFallback(f3);
  • 키가 "a" 인 HOCON 개체의 값을 요청하는 경우 다음 코드를 사용합니다.
var a = yourConfig.GetString("a");
  • 그러면 내부 HOCON 엔진은 키 a 에 대한 정의를 포함하는 첫 번째 HOCON 파일과 일치합니다.
  • 이 경우 f0은 "bar" 값을 반환합니다.

"foo"가 "a" 의 값으로 반환되지 않은 이유는 무엇인가요?

  • HOCON은 Config 체인에서 일치하는 항목이 이전에 발견되지 않은 경우에만 대체 config 객체를 검색하기 때문입니다.
  • 최상위 Config 객체에 a와 일치하는 항목이 있으면 대체 항목이 검색되지 않습니다.
  • 이 경우 f0 에서 a에 대한 일치가 발견되었으므로 f3의 a = foo 에는 도달하지 않았습니다.

HOCON 키 미스가 발생하면 어떻게 되나요?

  • c가 f0 또는 f1 에 정의되어 있지 않은 경우 다음 코드를 실행하면 어떻게 되나요?
var c = yourConfig.GetString("c");

  • 이 경우 yourConfig는 f2 까지 두 번 풀백되고 c의 키 값으로 "baz"를 반환합니다.
  • 이제 HOCON을 이해했으니, 이것을 사용하여 ChartingActor의 Dispatcher를 수정해 보겠습니다.

실습

  • 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 : UntypedActor
{
#region Messages

    public class InitializeChart
    {
        public InitializeChart(Dictionary<string, Series> initialSeries)
        {
            InitialSeries = initialSeries;
        }

        public Dictionary<string, Series> InitialSeries { get; private set; }
    }

    #endregion

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

    protected override void OnReceive(object message)
    {
        if (message is InitializeChart)
        {
            var ic = message as InitializeChart;
            HandleInitialize(ic);
        }
    }

    #region Individual Message Type Handlers

    private void HandleInitialize(InitializeChart ic)
    {
        if (ic.InitialSeries != null)
        {
            //swap the two series out
            _seriesIndex = ic.InitialSeries;
        }

        //delete any existing series
        _chart.Series.Clear();

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

    #endregion
}

}

* **App.config**
```xml
<?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;
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();
        }
    }
}
  • Form1.Designer.cs

namespace Akka_Test
{
partial class Form1
{
///


/// 필수 디자이너 변수입니다.
///

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 chartArea1 = new System.Windows.Forms.DataVisualization.Charting.ChartArea();
        System.Windows.Forms.DataVisualization.Charting.Legend legend1 = new System.Windows.Forms.DataVisualization.Charting.Legend();
        System.Windows.Forms.DataVisualization.Charting.Series series1 = new System.Windows.Forms.DataVisualization.Charting.Series();
        this.panel1 = new System.Windows.Forms.Panel();
        this.sysChart = new System.Windows.Forms.DataVisualization.Charting.Chart();
        this.panel1.SuspendLayout();
        ((System.ComponentModel.ISupportInitialize)(this.sysChart)).BeginInit();
        this.SuspendLayout();
        // 
        // panel1
        // 
        this.panel1.Controls.Add(this.sysChart);
        this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
        this.panel1.Location = new System.Drawing.Point(0, 0);
        this.panel1.Name = "panel1";
        this.panel1.Size = new System.Drawing.Size(800, 450);
        this.panel1.TabIndex = 0;
        // 
        // sysChart
        // 
        chartArea1.Name = "ChartArea1";
        this.sysChart.ChartAreas.Add(chartArea1);
        this.sysChart.Dock = System.Windows.Forms.DockStyle.Fill;
        legend1.Name = "Legend1";
        this.sysChart.Legends.Add(legend1);
        this.sysChart.Location = new System.Drawing.Point(0, 0);
        this.sysChart.Name = "sysChart";
        series1.ChartArea = "ChartArea1";
        series1.Legend = "Legend1";
        series1.Name = "Series1";
        this.sysChart.Series.Add(series1);
        this.sysChart.Size = new System.Drawing.Size(800, 450);
        this.sysChart.TabIndex = 1;
        this.sysChart.Text = "sysChart";
        // 
        // 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.panel1);
        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.panel1.ResumeLayout(false);
        ((System.ComponentModel.ISupportInitialize)(this.sysChart)).EndInit();
        this.ResumeLayout(false);

    }

    #endregion

    private System.Windows.Forms.Panel panel1;
    private System.Windows.Forms.DataVisualization.Charting.Chart sysChart;
}

}


* **Program.cs**
```csharp
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