NGMsoftware

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

    학습


    C# C# .NET 매크로 프로그램 만들기. (패러럴 처리가 가능한 비동기 반복 스크립트 만들기)

    페이지 정보

    본문

    안녕하세요. 엔지엠소프트웨어입니다. 이전 시간에 패러럴하게 실행할 수 있는 그룹 액션에 대해서 알아봤습니다. 이전 버전에서 불안정한 스레딩으로 여러가지 문제가 많았는데요. 새로운 버전은 멀티 환경에 최적화될 수 있도록 많은 부분들이 개선되고 있어요. 패러럴 기능에 대해서 좀 더 자세하게 알고 싶으면, 이전 글을 한번 읽어보시기 바랍니다. 오늘은 For 반복에 대해서 알아볼텐데요. 포 반복 액션은 하위로 추가한 액션들을 사용자가 설정한 횟수만큼 반복시켜주는 액션입니다.

     

    엔지엠 매크로 6에서는 스레드 안정성 문제로 비동기 기능을 제대로 사용할 수 없습니다. 스레드 문제뿐만 아니라 구조적으로 개선이 불가능한 상황이었어요. 엔지엠 매크로 6은 디자인 당시에 멀티 환경을 고려하지 않고, 싱글 환경으로 개발한 다음에 멀티 기능을 붙이다보니 기본적인 구조가 싱글에 최적화 되어 있었습니다. 그래서, 엔지엠 7 버전은 처음부터 멀티 환경을 고려해서 디자인했고, 구조도 유연하게 처리할 수 있도록 변경했습니다. 상당히 많은 부분이 변경되고, 매크로 프로그램의 근간이 되는 뼈대가 완전히 변경되었기 때문에 사용 방법도 어느정도 변경이 필요했습니다. 그리고, 불필요한 속성들을 제거해서 복잡도를 상당부분 낮췄습니다.

     

    일단, 포(For) 반복과 포이치 반복(Foreach)은 프로그래밍 언어라서 다른 명칭으로 변경할까도 생각했는데요. 이미 엔지엠 3부터 쭉 사용해오던 이름이라서 그대로 가기로 했습니다. 이전 사용자분들이 동일한 기능을 찾는데 어려움이 있으면 안되니까요. 그리고, 적절한 이름을 찾기가 쉽지 않았어요^^

     

    모델에 ForModel과 ForeachModel을 2개 추가했습니다. ForeachModel은 아직 개발이 완료되지 않았지만, 미리 추가 해두었습니다. 아마도, 다음에 글을 작성할거 같네요.

    QHkMxCe.png

     

     

    ForModel도 GroupModel과 동일하게 IIndependentModel 인터페이스를 상속 받아야 합니다. 비동기 처리가 필요한 모델들은 모두 이 인터페이스를 구현해야 합니다.

    public class ForModel : ActionModel, Ai.Interface.IIndependentModel

     

    아래 속성이 필수입니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("UseAsynchronous")]
    [LocalizedDescription("UseAsynchronous")]
    [Browsable(true)]
    [DefaultValue(false)]
    public bool UseAsynchronous { get; set; }

     

    반복할 횟수를 사용자가 입력할 수 있도록 속성을 하나 만들어줍니다. 이 속성의 기본 값은 1입니다. 무조건 1번은 실행되어야 합니다. 만약, 1보다 작은 값으로 설정하면 반복 횟수가 0이므로 액션이 하나도 실행되지 않습니다. 추후에 반복 횟수 속성을 가진 액션들이 자주 등장할텐데요. 전부 기본값은 1입니다.

    [LocalizedCategory("Action")]
    [LocalizedDisplayName("CompareRepeat")]
    [LocalizedDescription("RepeatCount")]
    [Browsable(true)]
    [DefaultValue(1)]
    public int RepeatCount { get; set; } = 1;

     

    Task를 추가해서 처리하는 액션이 그룹 모델뿐이어서 메소드 이름을 Function으로 만들었는데요. 새로운 비동기 모델들이 계속 추가될 예정이기 때문에 이름을 SubTask로 변경했습니다. 아무래도 Function 보다는 좀 더 추상적인 이름을 사용하는게 좋을거라는 판단입니다. 범용적으로 사용하기 위해서는요.

    public override string? Execute(IPlayer player)
    {
        if (UseAsynchronous && !Ai.Common.Helper.NullCheckAndWriteLine(player, nameof(ID), ID))
            return null;
    
        if (UseAsynchronous && Actions?.Count > 0)
            player.SubTask(ID, Actions.Cast<IModel>().ToList(), Ai.Definition.ActionType.For, RepeatCount);
    
        return null;
    }

     

     

    플레이어쪽은 많은 부분이 리팩토링되었습니다. 기존 Function 메소드의 내용도 아래와 같이 변경해야 합니다.

    public void SubTask(string taskId, List<IModel> actions, Ai.Definition.ActionType actionType, int repeatCount = 1)
    {
        Run(taskId, actions.Cast<ActionModel>().ToList(), actionType, repeatCount);
    }

     

    기존에 Run 메소드에서 모든 내용을 처리했었는데요. Run 메소드도 스크립트와 스크립트 안에서 비동기로 실행될 액션들만 따로 처리할 수 있도록 TaskRun으로 분리했습니다.

    private void Run(string scriptId, List<ActionModel> actions, Ai.Definition.ActionType actionType = Definition.ActionType.Script, int repeatCount = 1)
    {
        if (actions.Count() == 0)
            return;
    
        Color? backColor = null;
    
        _isCancel = false;
        Manager.Output.InfoWriteLine(
                       string.Format(Manager.Client.ResxMessage.GetString($"{actionType.ToString()}Start"), 
                       Path.GetFileNameWithoutExtension(scriptId)));
    
        _taskList.Add(scriptId, Task.Run(() =>
        {
            Task currentTask = _taskList[scriptId];
            TreeNode? beforeNode = null;
    
            if (!Script.IsBackground)
                backColor = actions.First().TreeNode?.BackColor;
    
            Ai.Interface.IEditor? editor = null;
            if (Manager.Client is Ai.Interface.IEditor)
                editor = (Ai.Interface.IEditor)Manager.Client;
    
            TaskRun(editor, currentTask, actions, beforeNode, actionType, repeatCount);
    
            // TODO 트레이스 모드에서는 아래 코드 실행 안함
            //if (!Script.IsBackground && beforeNode != null)
            //{
            //    beforeNode.BackColor = backColor;
            //    beforeNode.NodeFont = null;
            //}
    
            if (!Script.IsBackground)
                Parallel.ForEach(actions, action => action.TreeNode = null);
    
            Dispose(scriptId, actionType);
        }, CT));
    }

     

    반복 횟수에 따라서 액션만 실행하면 되는 부분이고, 별도로 태스크를 다시 만들 필요는 없습니다. 처음 테스트용 디자인에서는 서브 액션들을 모두 매인 스크립트에서 제거하고, 별도의 타스크에서 실행하려고 했는데요. 이렇게하면 매인 스크립트를 앞서 개발한 일시 중지로 중지시키고, 반복용 액션들을 별도의 타스크로 실행 후 매인을 다시 실행해야 합니다. 이렇게 디자인한 후 테스트를 해봤는데요. 가용할 수 있는 태스크를 요청하고, 기존 태스크를 중지시킨 후 처리하는게 많은 비용이 발생하는걸 확인했습니다. 프로세싱을 최적화 하기 위해 비동기 모드인 경우에만 태스크로 처리하고, 그 외에는 전부 로직으로 처리할 수 있도록 해야겠습니다.

     

    별도로 뺀 TaskRun은 별도의 태스크를 생성하지는 않고, 기존 플레이어의 태스크 안에서 반복만을 위한 옵션이 추가되었습니다.

    for (int x = 0; x < repeatCount; x++)
    {
        for (int y = 0; y < actions.Count(); y++)
        {
            string? id = action.Execute(this);
    
            if (string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(_reservedGotoId))
            {
                id = _reservedGotoId;
                _reservedGotoId = null;
            }
    
            if (editor != null)
                editor.Property.Refresh();
    
            Manager.Output.WriteLine(action.ToString());
    
            if (!string.IsNullOrEmpty(id))
                _reservedGotoId = GoToID(ref y, actions, id);
        }
    }

     

    이렇게하면 포 반복(For Iteration) 액션을 비동기 또는 동기 모드로 실행할 수 있게됩니다. 반복기(For, Foreach, ETC) 액션들은 기본적으로 모두 이와 비슷한 로직을 따라가면 됩니다. 이제 정상적으로 동작하는지 테스트 해볼까요? 테스트 스크립트는 아래와 같이 간단하게 만들었습니다. 반복 횟수는 5입니다.

    n188A1Z.png

     

     

    매크로를 실행하면 아래와 같이 메모장을 클릭하고, 메모장에 a 단어를 5회 입력합니다. 마지막으로 클릭 액션까지 동작하고 스크립트가 완료됩니다.

     

     

    포 반복에 입력한 반복 횟수만큼 키보드가 반복해서 a를 메모장에 입력했습니다. 이번에는 액션 이동으로 반복 루틴을 빠져 나가도록 해볼께요. 아래와 같이 액션 이동에서 b 액션으로 이동하도록 처리하고, 마우스 클릭도 하나 더 추가했습니다. 로직대로라면 내문서는 클릭하지 않고, 휴지통만 클릭해야 합니다.

    1dUHLoz.png

     

     

    매크로를 실행하면 For 반복 액션이 실행되고, a를 한번 입력합니다. 그리고, 액션 이동이 b로 루틴을 이동시키기 때문에 반복은 중지되고 마지막 클릭이 동작하게 됩니다.

     

     

    마지막으로 비동기 모드에서 반복을 알아봅시다. 아래 그림과 같이 포 반복 액션의 비동기 사용을 True로 변경하세요.

    V5EOgJh.png

     

     

    매크로를 실행하면 메모장에 a키를 1번 누르고, 내문서 클릭 및 휴지통 클릭이 됩니다. 그리고, 스크립트가 완료됩니다. 다만, 동기 모드와 다른점은 시간에 차이가 있습니다.

     

     

    비동기 모드라도, 매인 태스크로 루틴을 이동할 수 있습니다. 하지만, 우리가 생각하는 것과 동작이 다릅니다. 예상은 비동기 모드로 키보드 a를 누르고 액션 이동이 실행되서 마지막 클릭인 a가 실행되어야 합니다. 하지만 위의 동영상에서는 내문서도 클릭하고 휴지통도 클릭합니다. 이렇게 동작하는 이유는 For 반복 액션이 비동기로 실행되고 액션 이동을 만나기 전에 키보드에서 이미 0.1초를 기다립니다. 이 시간이면 이미 내문서 클릭 액션이 실행중인 상태입니다.

     

    잘 이해가 안갈수도 있는데요. 액션 이동이 실행되기 전 1초 지연이 있는 내문서 클릭이 이미 실행 예약이 되어 있는 상태라서 실행이 된 후 액션이 이동된겁니다. 결국은 타이밍 문제입니다. 그래서, 지연 액션과 같이 별도의 액션을 사용하면 클릭 액션에서 지연을 주는것과 다른 결과를 나타내게 됩니다. 아직 지연 액션을 따로 만들지는 않았는데요. 액션이 분리되어야만 원하는 동작을 만들 수 있어서 꼭 필요한 액션이기도 합니다.

     

    이번에는 내문서 클릭 액션 위에 뭔가 하나를 추가하고, 다시 실행 해볼께요.
    kkM1HyG.png

     

     

    예상한 시나리오대로 동작하는걸 확인할 수 있습니다. 중간에 추가한 설명 액션이 실행중에 액션 이동이 실행되어서 내문서를 건너뛰고 휴지통을 클릭했습니다. 비동기 모드에서는 시나리오 구성이나 테스트가 정말 어렵습니다. 타이밍이라는게 의도한것처럼 동작하지 않을수도 있거든요. 그리고, 무엇보다 어려운점은 이런 시나리오를 계속해서 연습하고 예측하지 않으면 어디에서 문제인지조차 가늠하기가 쉽지 않습니다.

     

     

    아래 액션이 흘러간 흔적을 보면, 액션 이동 아래에 키보드 입력은 실행이 안되었고, 액션 이동이 보낸 a 클릭은 동작했지만, 그 위의 클릭은 동작하지 않았습니다.

    woPccmT.png

     

     

    아직 다양한 시나리오에서 테스트하진 않았지만, 좀 더 디테일한 부분까지 테스트를 해보고 마무리 해야할거 같아요. 비동기 처리와 관련된 다른 액션들이 중첩되었을 때도 어떻게 동작할지 검증해봐야 할텐데요. 이부분들이 생각보다 어려울듯 합니다. 다소 시간이 걸리겠지만~ 하나씩 풀어나간다면 완성되지 않을까요^^

     

    개발자에게 후원하기

    MGtdv7r.png

     

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

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

    감사합니다~

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

    댓글목록

    등록된 댓글이 없습니다.