본문 바로가기
Utils

C# Windowform, 유니티처럼 코루틴 만들어서 사용하기

by Client. DJ 2022. 7. 17.
반응형

여러 Form에서도 사용 편리한 코루틴
윈도우 폼에서 코루틴 사용하기

유니티의 코루틴 개념은 편했다.

본래 유니티 프로그래머로 시작한 저한테, 기존 Windowform 접근하는데 불편한 점이 이만저만이 아니었습니다. 개인적으로 유니티의 코루틴 반복기 개념이 편하고 좋아서 개인 아카이브 저장한다는 생각으로 작성하고 올립니다.

장점

  • 별도의 반복문을 메인 쓰레드에서 분리하여 따로 돌리고 싶은 경우가 생기는데, 코루틴 사용으로 크로스 쓰레드를 회피할 수 있습니다. (Avoiding Cross-Thread, Safety Thread)
  • 코루틴의 장점인 스케줄링(Scheduling)이 가능합니다.
  • UI 쓰레드에 접근이 가능합니다. (매우 용이)

스크립트

아래 2개의 스크립트에 프로젝트에 추가하시면 사용할 수 있습니다.

 

1. Coroutine.cs

using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace System.Windows.Forms
{
    public class Coroutine
    {
        public Coroutine parent;
        public Control control;
        public IEnumerator enumerator;
        public bool isWait = false;
    }

    public static class CoroutineSchedule
    {
        private const int UPDATE_INTERVAL = 100;    // 0.1초

        private static List<Coroutine> coroutines = new List<Coroutine>();
        private static List<Coroutine> waitAdds = new List<Coroutine>();
        private static List<Coroutine> waitRemoves = new List<Coroutine>();

        private static bool isStarted = false;

        /// <summary>
        /// 코루틴 시작
        /// </summary>
        /// <param name="control"></param>
        /// <param name="enumerator"></param>
        /// <returns></returns>
        public static Coroutine StartCoroutine(this Control control, IEnumerator enumerator)
        {
            control.HandleDestroyed += HandleDestroyed;
            Coroutine coroutine = new Coroutine { control = control, enumerator = enumerator };
            coroutines.Add(coroutine);
            if (!isStarted) Updater();
            return coroutine;
        }

        /// <summary>
        /// 코루틴 중지
        /// </summary>
        /// <param name="control"></param>
        /// <param name="coroutine"></param>
        public static void StopCoroutine(this Control control, Coroutine coroutine)
        {
            if (coroutine != null)
                waitRemoves.Add(coroutine);
        }

        /// <summary>
        /// 핸들러 소멸 이벤트 (코루틴에서 삭제 처리)
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void HandleDestroyed(object sender, EventArgs e)
        {
            waitRemoves.Add(coroutines.Find(co => co.control.Equals(sender)));
        }

        /// <summary>
        /// 코루틴 연장 처리
        /// </summary>
        /// <param name="coroutineInfo"></param>
        private static void YieldCoroutine(Coroutine coroutineInfo)
        {
            coroutineInfo.control.HandleDestroyed += HandleDestroyed;
            coroutines.Add(new Coroutine { parent = coroutineInfo, control = coroutineInfo.control, enumerator = coroutineInfo.enumerator.Current as IEnumerator });
        }

        /// <summary>
        /// Updater
        /// </summary>
        public static async void Updater()
        {
            isStarted = true;
            while (coroutines.Count > 0)
            {
                // 반복기 체크
                for (int i = 0; i < coroutines.Count; i++)
                {
                    IEnumerator enumerator = coroutines[i].enumerator;
                    if (enumerator != null)
                    {
                        if (enumerator.Current == null)
                            enumerator.MoveNext();

                        if (enumerator != null)
                        {
                            iKeepWait keepWait = enumerator.Current as iKeepWait;
                            if (keepWait != null && keepWait.IsMoveNext())
                            {
                                var isEnd = !enumerator.MoveNext();
                                if (isEnd)
                                {
                                    waitRemoves.Add(coroutines[i]);
                                    if (coroutines[i].parent != null)
                                    {
                                        coroutines[i].parent.isWait = false;
                                        var isEndParent = !coroutines[i].parent.enumerator.MoveNext();
                                        if (isEndParent)
                                        {
                                            waitRemoves.Add(coroutines[i].parent);
                                        }
                                    }
                                }
                            }
                            else
                            {
                                if (!coroutines[i].isWait)
                                {
                                    IEnumerator newEnumerator = enumerator.Current as IEnumerator;
                                    if (newEnumerator != null)
                                    {
                                        coroutines[i].isWait = true;
                                        waitAdds.Add(coroutines[i]);
                                    }
                                }
                            }
                        }
                    }
                }

                // 반복기 종료 처리
                for (int i = 0; i < waitRemoves.Count; i++)
                {
                    waitRemoves[i].control.HandleDestroyed -= HandleDestroyed;
                    coroutines.Remove(waitRemoves[i]);
                }
                waitRemoves.Clear();

                // 반복기에서 반복기로 연장되는 경우
                for (int i = 0; i < waitAdds.Count; i++)
                {
                    YieldCoroutine(waitAdds[i]);
                }
                waitAdds.Clear();

                // 반복 주기
                await Task.Delay(UPDATE_INTERVAL);
            }
            isStarted = false;
        }
    }
}

2. iKeepWait.cs

using System;

public interface iKeepWait
{
    bool IsMoveNext();
}

public class WaitForSeconds : iKeepWait
{
    private float _seconds;
    private DateTime after;

    public WaitForSeconds(float seconds)
    {
        _seconds = seconds;
        after = DateTime.Now.AddSeconds(_seconds);
    }

    public bool IsMoveNext()
    {
        return after <= DateTime.Now;
    }
}

public class WaitUntil : iKeepWait
{
    public delegate bool Condition();
    private Condition _condition;

    public WaitUntil(Condition condition)
    {
        _condition = condition;
    }

    public bool IsMoveNext()
    {
        return _condition.Invoke();
    }
}

public class WaitWhile : iKeepWait
{
    public delegate bool Condition();
    private Condition _condition;

    public WaitWhile(Condition condition)
    {
        _condition = condition;
    }

    public bool IsMoveNext()
    {
        return !_condition.Invoke();
    }
}

예제

Control 클래스의 확장 클래스로 작성되어기에 this.StartCorouine() 사용하시면 되겠습니다.

유니티 코루틴 개념을 그대로 따라갑니다. 원리는 yield 체크하는 부분이 await하고 있다가 다시 원래 쓰레드로 돌려주며 처리됩니다. (여기서는 await과 async 개념 이해가 필요합니다.)

private void Form1_Load(object sender, EventArgs e)
{
    this.StartCoroutine(Test());
}

int count = 0;
private IEnumerator Test()
{
    // 시간 기다리기
    yield return new WaitForSeconds(0.1f);
    metroLabel1.Text = "코루틴 테스트" + (++count);

    // 1틱 기다리기
    yield return null;

    // 특정 조건 충족 때까지 기다리기
    DateTime now = Time.NowTime;
    yield return new WaitUntil(() => now.AddSeconds(2f) <= Time.NowTime);
    //yield return new WaitWhile(() => { /* todo... */});

    // 다른 반복기로 연결하고 끝날 때까지 기다리기
    yield return Test2();
    yield return Test2();
    yield return Test2();

    metroLabel1.Text = "코루틴 종료";
}

private IEnumerator Test2()
{
    yield return new WaitForSeconds(0.1f);
    metroLabel1.Text = "코루틴 테스트" + (++count);
}

예제 결과

마무리

아무래도 유니티 프로그래머로써 유용하게 사용했던 로직이어서 윈폼에서도 유용하게 사용하게 되었습니다. 윈폼을 사용하다보면 유니티와 다르게, 비동기를 접근해서 사용하게 되는 경우가 생기는데 크로스 쓰레드 에러가 발생합니다. 코루틴을 사용하게 되면 크로스 쓰레드되는 경우를 회피하며 쓰레드처럼 사용할 수 있습니다.

 

쓰레드의 병렬 처리에 대해서 이해를 잘못하다보면, 무분별한 쓰레드 사용을 하게 됩니다. 처음에 여러 시도를 하다보니 안전하게 스케쥴링이 가능한 코루틴이 생각이 들어 작성하게 되었습니다.

 

누군가에게는 도움이 바라며 글을 마칩니다. :)

반응형

댓글