NGMsoftware

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

    학습


    C# C# .NET Core 매크로 프로그램 만들기. (로그 출력창 만들기)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 오늘은 매크로를 제작할 때 동작을 분석하거나 에러 내용을 확인하기 위한 출력창을 만들어 보겠습니다. 우선, 출력창을 만들 때 미리 고민해야 할것들이 있는데요. 출력창은 여러개의 매크로 스크립트가 동시에 실행되더라도 스레드에 안전하게 메세지를 표시할 수 있어야 합니다. 그리고, 동시에 실행되는 멀티 스레딩 환경에서 지연 없이 화면에 출력해야 합니다.

     

    만약, 출력창이 동기화되어 있다면 어떤일이 발생할까요? 예를 들어서 동시에 10개의 매크로 스크립트가 실행중이라고 생각 해봅니다. 이 때 1번 스크립트의 출력 내용을 기록하기 위해 나머지 2~10번 스크립트가 대기해야 한다면 전체적으로 느려지는 결과가 만들어집니다. 이 문제를 해결하기 위해 별도의 큐를 만들고, 메세지를 관리해야 합니다. UI 스레드를 사용하는 큐도 별도의 스레드로 처리해야 합니다.

     

    위에 설명한 내용과 같이 출력창을 만들더라도 여전히 로그를 기록하는건 많은 비용이 발생하는 무거운 작업입니다. 엔지엠 6보다 퍼포먼스는 좋아졌지만, 그럼에도 불구하고 프로덕션 환경에서는 로그를 끄고 사용하시는게 좋습니다. 출력창에 로그를 표시하지 않으면 매크로 동작 퍼포먼스가 비약적으로 개선됩니다. 간단하게 디자인부터 해볼까요?

    dRh9QzU.jpeg

     

     

    AI 매크로 에디터의 하단 출력창을 보면 뭔가 버튼이 많이 추가되어 있는걸 확인할 수 있습니다. 엔지엠 매크로를 사용해오던 분들은 동일한 기능들이라서 새로울만한건 없을겁니다. 다만, 이전 출력 컨트롤을 많은 부분 개선해서 속도와 기능면에서 좋아졌습니다. 사용자 입장에서는 속도가 개선되었지만, 체감할 수 있는 수준은 아닐겁니다. 컨트롤을 사용하는 개발자 입장에서 기능들이 추가되었다는 의미입니다.

     

    출력창도 에디터와 플레이어 그리고, 디자이너에서 모두 사용할 수 있기 때문에 에디터용 디자인과 플레이어용 디자인이 구분되어 있습니다. IOutput 인터페이스를 상속 받아서 구현하면 Ai.Engine에서 보내주는 메세지를 받아서 화면에 출력할 수 있습니다.

    FssHiYz.jpeg

     

     

    인터페이스는 아래와 같습니다. 기본적으로 메세지 또는 텍스트와 같은 로그성 내용을 Output 컨트롤에 출력하기 위한 메소드들이 정의되어 있습니다.

    using System.Drawing;
    
    namespace Ai.Interface
    {
        public interface IOutput
        {
            /// <summary>
            /// 로그를 출력합니다.
            /// </summary>
            /// <param name="log"></param>
            void Write(string log);
    
            /// <summary>
            /// 로그를 사용자가 지정한 색으로 출력합니다.
            /// </summary>
            /// <param name="log"></param>
            /// <param name="foreColor"></param>
            void Write(string log, Color foreColor);
    
            /// <summary>
            /// 로그를 사용자가 지정한 색으로 출력하고, 폰트를 굵게 처리합니다.
            /// </summary>
            /// <param name="log"></param>
            /// <param name="foreColor"></param>
            /// <param name="isBold"></param>
            void Write(string log, Color foreColor, bool isBold);
    
            /// <summary>
            /// 로그를 출력하고, 줄바꿈을 추가합니다.
            /// </summary>
            /// <param name="log"></param>
            void WriteLine(string log);
    
            /// <summary>
            /// 로그를 사용자가 지정한 색으로 출력하고, 줄바꿈을 추가합니다.
            /// </summary>
            /// <param name="log"></param>
            /// <param name="foreColor"></param>
            void WriteLine(string log, Color foreColor);
    
            /// <summary>
            /// 로그를 사용자가 지정한 색으로 출력하고, 폰트를 굵게 처리 후 줄바꿈을 추가합니다.
            /// </summary>
            /// <param name="log"></param>
            /// <param name="foreColor"></param>
            /// <param name="isBold"></param>
            void WriteLine(string log, Color foreColor, bool isBold);
        }
    }

     

    컨트롤의 디자인을 보면 로그 파일을 저장하거나 지우기가 있고, 메모리 관리를 위해서 자동 삭제 기능이 포함되어 있습니다. 이런 부가적인 기능들은 크게 어려운 부분이 없어서 개발자라면 충분히 구현할 수 있을거예요. 핵심적인 기능은 위 인터페이스를 구현하는겁니다. 로그를 기록하는게 가장 큰 기능이니까요.

     

    기본 컨트롤은 RichTextBox입니다. 텍스트를 스레드로부터 안전하게 쓰기 위해 ConcurrentQueue를 사용했습니다. 스레드로부터 안전한 FIFO(선입선출) 방식의 컬렉션을 나타냅니다.

    private ConcurrentQueue<QueueTaskObject>? tasks;

     

    큐를 관리하기 위한 타스크 팩토리를 만들고, 큐에 아이템이 있는지 체크하면서 로그 텍스트가 들어오면, 하나씩 내보냅니다. 내보내 로그 텍스트는 삭제해줍니다.

                this.backgroundTask = Task.Factory.StartNew(async () =>
                {
                    try
                    {
                        while (!disposedValue)
                        {
                            this.cancellationTokenSource.Token.ThrowIfCancellationRequested();
                            if (this.tasks.Count > 0)
                            {
                                if (this.tasks.TryPeek(out QueueTaskObject? task))
                                {
                                    if (this.output.OriginalWrite(task.message, task.color, task.isBold, task.showTime))
                                        this.tasks.TryDequeue(out QueueTaskObject? r);
                                }
                            }
                            await Task.Delay(1, this.cancellationTokenSource.Token);
                        }
                    }
                    catch (OperationCanceledException) { }
                    catch (Exception) { }
                }, cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

     

    로그 텍스트를 큐에 넣어주는 메소드도 하나 추가했습니다.

            internal void Enqueue(QueueTaskObject task)
            {
                this.tasks?.Enqueue(task);
            }

     

    Ai.Engine의 플레이어에서 로그를 아웃풋에 씁니다. 인터페이스를 구현하고 있습니다.

            internal void Write(string text, Color color, bool isBold)
            {
                this._writeLineQueue?.Enqueue(new QueueTaskObject(text, color, isBold, _config.ShowTime));
            }

     

    추가적으로 아웃풋 콘트롤의 타스크를 실행하거나 중지할 수 있는 기능도 필요할거 같습니다. 에디터가 실행되자마자 백그라운드 스레드가 하나 돌기 때문입니다. 매크로를 실행하지 않으면 굳이 스레드를 돌릴필요가 없으니까요. 아니면 아웃풋 컨트롤 표시 여부에 따라서 리소스를 모두 해제하는 방법도 좋을듯 합니다. 우선, IDispose 인터페이스를 구현해야겠네요.

     

    아웃풋 콘트롤도 IClose 인터페이스를 상속받아서 구현하고 있기 때문에 창을 닫을 때 Close 메소드가 자동으로 호출됩니다.

    public partial class OutputContainer : UserControl, IOutput, ISave, IClose

     

    Close 메소드에서 콘트롤의 Dispose를 호출하면, OnHandleDestroyed 이벤트 알림을 받을 수 있습니다.

            public void Close()
            {
                this.Dispose();
            }

     

    실제 구현체가 있는 Output 컨트롤의 Dispose를 호출해줍니다.

            protected override void OnHandleDestroyed(EventArgs e)
            {
                this._output.Dispose();
                base.OnHandleDestroyed(e);
            }

     

    Output 컨트롤은 IDispose를 재정의해야 합니다. 자신이 가지고 있는 타스크들을 모두 메모리에서 해제해야 하기 때문입니다.

            protected override void Dispose(bool disposing)
            {
                if (!this.disposedValue)
                {
                    if (disposing)
                    {
                        this.cancellationTokenSource.Cancel(true);
                        this.backgroundTask.Dispose();
                        this.tasks?.Clear();
                    }
                }
            }

     

    아웃풋에 내용을 기록할 수 있도록 코드를 약간 변경한 후 테스트 해봤습니다. 아래와 같이 잘 동작하는걸 확인할 수 있습니다.

     

     

    아직 멀티 스레드에서 동시에 실행하진 않았는데요. 기본적인 기능들을 대략이라도 모두 구현해놓고, 멀티 매크로 스크립트 동작을 테스트 해봐야겠습니다. 그렇게하려면 일단은 핸들 추가 액션을 먼저 구현하고, 다수의 스크립트를 복사해서 동시에 실행해보면 될거 같아요. 아마도 몇일내로 가능하지 않을까 생각되는데... 뭔가 일이 생기면 좀 더 늦어질수도 있을거 같긴 합니다. 멀티 스레딩 환경을 연출하기 위해 아래와 같이 5개의 스레드가 50번의 로그를 기록하도록 했습니다.

                Action<int, int> action = (taskNumber, index) => player.Output.WriteLine($"TaskNumber:\t{taskNumber}\tIndex:{index}");
                List<Task> taskList = new List<Task>();
    
                for (int t = 1; t <= 5; t++)
                {
                    int tn = t;
                    taskList.Add(Task.Run(() =>
                    {
                        for (int n = 1; n <= 50; n++)
                        {
                            int nn = n;
                            taskList.Add(Task.Run(() => action(tn, nn)));
                        }
                    }));
                }
                Task.WaitAll(taskList.ToArray());

     

    결과는 아래와 같은데요. 스레드라서 순서가 보장되지 않습니다.

    vFMWfnO.jpeg

     

     

    개발자에게 후원하기

    MGtdv7r.png

     

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

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

    감사합니다~

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

    댓글목록

    등록된 댓글이 없습니다.