NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# C# .NET Core 매크로 프로그램 만들기. (멀티 다클라 매크로 1부)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 이전 시간에 비활성 모드에서 하나의 스크립트로 멀티 다클라를 만들어봤습니다. 이전 버전의 엔지엠 매크로는 플레이어 하나당 핸들을 하나만 할당할 수 있었는데요. 이로인해 스크립트 하나로 여러개의 핸들을 등록해서 동작시키려면 복잡한 단계를 거쳐야 합니다. 엔지엠 6에서 멀티 다클라가 안되는건 아닙니다. 플레이어의 기능에 멀티 스크립트와 멀티 핸들 스크립트가 있어서 여러개의 핸들을 처리할 수 있긴합니다.

     

    새로운 버전에서는 좀 더 간단하게 핸들을 추가하고, 동기 모드와 비동기 모드로 멀티 다클라 매크로를 제작할 수 있습니다. 관리도 쉬워졌습니다. 처음 이 부분을 디자인할 때 플레이어 클래스 하나로 모두 처리하려고 했는데요. 등록된 핸들 수만큼 내부에서 플레이어를 생성해서 관리하면 될줄 알았습니다. 하지만, 여러개의 스크립트를 동시에 실행하면 문제가 발생합니다. 하나의 스크립트를 복사해서 사용하는게 아닌 각각의 스크립트를 열어서 동시에 실행하는걸 생각하지 못했었거든요. 그래서, Ai.Engine의 플레이어를 관리할 수 있는 상위 개념의 클래스가 필요합니다. Manager라고 명명했어요.

     

    매니저는 스레드로 실행되는 모든 플레이어를 관리해야 하기 때문에 싱글톤(Singleton)으로 만들었습니다. 싱글톤에 대해서 여러가지 갑론을박이 있습니다. 멀티 스레딩 환경에서 Thread-safe를 보장하는지에 대한 얘기입니다. 이 문제를 해결하기 위해 2중 랩핑을 사용하거나 레이지 인스턴스를 사용하기도 합니다. 저는 레이지를 사용하기로 했습니다. 그래서, 아래와 같이 레이지 맴버를 프라이빗으로 만들었습니다.

    private static Lazy<Manager>? _lazy = null;

     

    싱글톤이기 때문에 생성자는 프라이빗입니다. new로 인스턴스화 하는걸 막기 위함입니다. 인자로 클라이언트(에디터, 플레이어, 디자이너등등...)를 받아서 맴버에 저장합니다.

            private Manager(Ai.Interface.IClient client)
            {
                this.Client = client;
                this.Output = client.Output;
                this.Option = (Ai.Interface.IOption)client.Option;
            }

     

    매니저를 인스턴스화 할 수 없기 때문에 GetInstance 메소드를 하나 열어줍니다.

            public static Manager GetInstance(Ai.Interface.IClient client)
            {
                if (_lazy == null)
                    _lazy = new Lazy<Manager>(() => new Manager(client));
    
                return _lazy.Value;
            }

     

    매니저는 봉인(Sealed) 클래스로 만들어줍니다. 확장하지 못하도록 막기 위함입니다. 매니저 인스턴스는 클라이언트 라이프 사이클에서 오직 하나만 존재해야 합니다.

    public sealed class Manager : Ai.Interface.IPlayerManager

     

    멀티 다클라 매크로 환경에서 여러개의 스크립트를 열고, 동시에 실행하더라도 매니저는 하나만 만들어지고 더이상 생성되지 않습니다. 그리고, 매니저가 스크립트마다 하나의 플레이어를 생성하고 관리합니다. 스크립트를 중지하거나 완료되면 매니저는 메모리에서 삭제합니다. 이렇게 동작하려면 플레이어들을 관리할 컬렉션이 필요합니다. 스레드에 안전한 방식으로 컬렉션을 관리해야 하므로 컨커런트 딕셔너리(ConcurrentDictionary)를 사용합니다.

    public ConcurrentDictionary<string, IPlayer> Players { get; private set; } = new ConcurrentDictionary<string, IPlayer>();

     

    ConcurrentDictionary는 다중 스레드 시나리오를 위해 설계되었습니다. 이 컬렉션에서 항목을 추가하거나 제거하기 위해 코드에 잠금(Lock)을 사용할 필요는 없습니다. 그러나 동일한 키에 새 값을 지정하여 한 스레드에서 값을 검색하고 다른 스레드에서 컬렉션을 바로 업데이트하는 것은 언제든지 가능합니다. 이 부분이 엔지엠 6에서는 불가능했습니다. 스레드에 안전하지 않은 컬렉션을 사용했기 때문입니다. 그래서, 플레이어를 추척하고 조작할 때마다 잠금(Lock)을 사용해야 했습니다. 잠금은 닷넷에서 가장 큰 비용을 발생시키는 작업중에 하나입니다.

     

    이제 매니저를 통해 플레이어를 생성하고, 실행할 수 있습니다. 아직 모두 구현된 코드는 아니지만, 아래와 같이 실행할 수 있습니다.

            public void Play(IScriptView script)
            {
                if (Players.Keys.Contains(script.Id))
                {
    
                }
                else
                {
                    if (Players.TryAdd(script.Id, new Player(this, script)))
                    {
                        Players[script.Id].Play();
                    }
                    else
                    {
    
                    }
                }
            }

     

    플레이어에 추가되어 있던 싱글톤 로직은 모두 제거했습니다. 그리고, 플레이어가 완료되면 목록에서 제거하도록 코드를 변경했습니다. 아직 깔끔한 코드는 아닙니다. 어떤 문제가 발생했을 때 조치해야 하는 방어 코드가 작성되지 않았는데요. 디테일한 부분들은 나중에 추가하도록 해야겠습니다. 지금은 처리해야 할 코드가 너무 많아서 테스트하면서 진행하기엔 무리가 따릅니다. 예측 코드를 미리 작성하는건 좋지 않은 습관이거든요.

            public void Remove(IPlayer player)
            {
                Players.TryRemove(new KeyValuePair<string, IPlayer>(player.Id, player));
            }

     

    플레이어의 플레이 메소드는 아래와 같습니다. 생성자에서 클라이언트로부터 받은 스크립트에 액션이 하나라도 있는지 체크합니다. 빈 스크립트면 사용자에게 어떤 방식으로든 알려줘야겠지만, 지금은 일단 종료되도록 처리 했습니다. 액션에 포함된 모델을 가져와서 목록화하고, 스크립트가 비동기 모드인지 체크합니다. 비동기 모드의 범위안에 있는 모든 스크립트를 먼저 실행하고 액션 목록에서 제거합니다. 그리고, 나머지 액션들을 하나씩 실행 해주도록 구성했습니다.

            public void Play()
            {
                if (Script.Actions == null || Script.Actions.Count == 0)
                    return;
    
                var actions = Script.Actions.Cast<TreeNode>().Where(w => w.Tag is Model.Action.ActionModel).Select(s => (Model.Action.ActionModel)s.Tag).ToList();
                var addHandleModel = actions.Where(w => w is Model.Action.Application.AddHandleModel).Where(w => ((Model.Action.Application.AddHandleModel)w).UseInactiveMode).FirstOrDefault();
                bool isAsynchronous = false;
    
                if (addHandleModel != null)
                {
                    foreach (var action in actions)
                    {
                        Manager.Output.WriteLine($"{action.DisplayText}", Color.Blue, isBold: true);
                        action.Excute(this);
                        Manager.Output.WriteLine(action.ToString(), Color.Blue);
    
                        if (action == addHandleModel)
                            break;
                    }
    
                    if (MainHandles == null || MainHandles.Count == 0)
                        isAsynchronous = false;
                    else
                        isAsynchronous = true;
    
                    actions.RemoveRange(0, actions.IndexOf(addHandleModel) + 1);
                }
    
                if (Script.Actions == null || Script.Actions.Count == 0)
                    return;
    
                if (isAsynchronous)
                {
                    foreach (var handle in MainHandles)
                    {
                        var player = new Player(this);
                        player.MainHandles = new List<IntPtr> { handle };
                        player.ControlHandles = new List<IntPtr> { ControlHandles[MainHandles.IndexOf(handle)] };
                        player.Run($"{Script.Id} {handle}", actions);
                    }
    
                    Stop();
                }
                else
                    Run(Script.Id, actions);
            }

     

    기본 골격을 모두 만들어진거 같은데요. 정상적으로 동작하는지 테스트를 해봐야겠네요. 멀티 스크립트에 대한 테스트는 아닙니다. 변경점이 많아서 기존 동작이 정상인지 확인하는 테스트입니다. 이전 내용과 동일한 테스트인데요. 메모장을 여러개 실행 해두고, 동시에 글이 작성되는지 확인합니다.

     

     

    로그도 정상적으로 출력되고 있습니다. 비동기로 입력해야 할 프로그램이 총 12개고, 쓰기 액션도 12번 동작했네요.

    Welcome to NGMsoftware!
    http://ngmsoftware.com
    장치 입력 방법
    핸들 추가
    매인 핸들:133380
    컨트롤 핸들:133384
    매인 핸들 목록:133380,5900216,1050444,395536,657646,591978,1312828,1181890,1181702,133482,2689138,199082
    컨트롤 핸들 목록:133384,395254,2361192,1442512,2361330,133130,198952,198858,67940,1574930,723056,68036
    프로세스 아이디:24084
    
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    쓰기
    

     

    다행히 잘 동작하는군요. 다음에는 여러개의 스크립트를 열어놓고 동시에 실행할 수 있는 글로벌 실행 버튼을 하나 만들어야겠습니다. 그리고, 변수 관련한 내용도 같이 처리 해보도록 할께요. 아직 가야할 길이 많이 남아있는데요. 하나씩 테스트하고, 퍼포먼스를 어떻게 향상시킬지에 대해 공부하면서 하다보니 시간이 오래 걸리고 있습니다. 기존에 엔지엠 6에 엄청나게 많은 기능들이 포함되어 있다보니 하루가 금방금방 가는군요^^

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.