코루틴(Coroutine) – [C# 시리즈 1]

오늘 진행할 내용은 코루틴이라는 개념이다.

우리가 Coroutine을 사용해야하는 이유는 Coroutine을 사용하면 비동기 처리가 너무나도 쉽게 이루어 질 수 있기 때문이라고 생각한다

Coroutine은 다양한 언어에서 지원하며, 언어마다 각자의 특색을 가지고 있다.

기본적인 특징이 있는데 아래와 같다.

  1. 특정 위치에서 실행을 일시 중단하고 다시 시작할 수 있는 여러 진입점을 허용한다.
  2. 비선점 멀티 태스킹을 위해 서브루틴을 일반화하는 프로그램의 구성 요소다.
  3. Thread Safe를 보장하진 않는다. (따로 구현해야 한다.)
  4. 처리 도중 취소가 가능하다.

여기서 메인 루틴과 서브 루틴이라는 개념이 나온다.

  • 메인 루틴 : 프로그램 전체의 개괄적인 동작 절차를 표시하도록 만들어진다.
  • 서브 루틴 : 메인 스레드가 서부루틴에 진입하면, 맨 윗줄부터 실행이 되고, return 문을 만나면 서브루틴을 호출했던 부분으로 탈출한다.

오해하면 안되는게 코루틴은 스레드가 아니다. 하나의 Object이며 스레드를 좀 더 효율적으로 사용하기 위한 기능이다.
또한 병렬 처리가 아니다. 동시성 처리로 이해해야 한다.
즉, Single Thread에서 더욱 효율적으로 사용하기 위해 만들어졌다는 것을 명심하자.

Coroutine을 사용하는 이유는 간단하다. 하나의 스레드에서 오랫동안 처리되는 작업을 진행하면 스레드가 대기 상태가 된다. 대기 상태에 있는 스레드를 다른 작업을 할 수 있도록 활용하기 위해 사용된다고 보면 된다.

img
Coroutine과 Function 진행 방식

위 이미지가 Coroutine의 사용 이유를 제대로 표현해주고 있다. Caller의 대기 시간이 현저하게 줄어든다. 

물론, 반환 받은 Caller가 할일이 없다면 그 작업은 async/await로 하는 것을 추천한다.

C#에서 Coroutine 반환 타입은 일반 반환과 다르다.

yield return value;

코루틴을 구현할 때 yield 키워드, IEnumerator 인터페이스를 활용한다.

메소드를 호출하면 메서드의 내용 전부가 실행되는 것이 아니다. yield return 까지만 실행되고 메서드 호출 부분으로 돌아간다. 그리고 다시 해당 메서드를 호출하면 return 한 부분 다음 부분이 실행된다.

예시 코드를 보자

public IEnumerator<int> LoopA()
{
    for(int i = 0; i < 100; i++)
    {
        Console.WriteLine(i);
        yield return i; // 1초 대기
    }
}

IEnumerator<int> LoopB()
{
    for(int j = 100; j < 200; j++)
    {
        Console.WriteLine(j);
        yield return j;
    }
}

void Start()
{
    Coroutine coroutine = new Coroutine();

    IEnumerator<int> coroutine1 = coroutine.LoopA();
    IEnumerator<int> coroutine2 = coroutine.LoopB();

    while(true)
    {
        int val1 = coroutine1.Current;
        int val2 = coroutine2.Current;
        Console.Write(val1 + val2);

        if(!coroutine1.MoveNext() || !coroutine2.MoveNext())
            break;
    }
}

위 코드는 StartCoroutine를 통해 Coroutine를 시작하는 코드다. LoopA에 있는 yield return을 통해 메서드가 반환되고, 다음 메서드인 LoopB가 실행된다. LoopB에서 yield return을 통해 다시 반환되고 LoopA가 실행된다.

코루틴은 열거자를 사용할 때 각 요소마다 값을 반환받고 그 사이사이 작업을 추가할 때 유용하다. 기존에 사용한 메서드는 열거자의 반복문이 전부 끝날 때까지 작업을 못하거나, 그 반복문 안에서 복잡하게 코드를 작성해야 했지만 코루틴을 이용하면 반복문 중간을 멈추게 하여 값을 반환받고, 메인 작업을 기존에 호출했던 곳에서 처리할 수 있다.

C#보다는 유니티에서 많이 사용하는 개념이며, UI를 멈추지 않고 각 이벤트 및 작업을 작게 쪼개어 처리할 수 있을까에서 시작된 고민을 해결한 기능이다.

결론적으로 코루틴 즉, yield를 사용하면 좋은 경우는 아래와 같다.

  • 거대한 데이터의 반복 작업을 중간중간 값을 반환받아야 할 경우
  • 유니티처럼 프레임 단위로 작업을 할 때, 배열의 값을 하나씩 반환받아야 할 경우

위와 같은 경우 코루틴을 사용하는 것이 좋다고 생각한다.

번외) 비동기 처리(Await/Async)와 코루틴의 다른 점은?

비동기 처리와 코루틴은 다른 상황에서 사용된다. 특히 백엔드 개발에서는 Network 통신이나 File I/O 등에서 비동기 처리를 사용한다. 처리 중간중간 반환받아야 하는 경우가 생각보다 적기 때문이다.

유니티에서는 UI에서 이벤트 처리 및 작업 처리 시 프레임 드롭을 막기 위해서는 하나의 큰 작업을 돌리는 것보단 작업 하나가 끝났을 때 바로 값을 반환받는 방식을 채택한다.

물론 백엔드나 유니티와 같은 UI가 메인인 작업에서도 상황에 따라 비동기 처리, 코루틴을 사용할 것이다.

필자가 공부를 하면서도 이해하는데 오래 걸렸던 만큼 틀린 부분이나 잘못된 부분은 댓글로 부탁드립니다.

읽어보면 좋은 글

C#으로 본 MVVM 패턴 정리 및 활용

참고

코틀린 코루틴(coroutine) 개념 익히기

Leave a Comment