소소한 나의 하루들

유니티 코루틴(Coroutine) / 서브루틴(Sub Routine) +α 본문

개발/유니티

유니티 코루틴(Coroutine) / 서브루틴(Sub Routine) +α

소소한 나의 하루 2024. 1. 27. 00:57

https://docs.unity3d.com/kr/2019.4/Manual/JobSystemMultithreading.html

 

멀티스레딩이란? - Unity 매뉴얼

단일 스레드 컴퓨팅 시스템에서는 한 번에 하나의 명령어가 입력되고 한 번에 하나의 결과가 출력됩니다. 프로그램을 로드하고 완료하는 데 걸리는 시간은 CPU가 수행해야 하는 작업량에 따라

docs.unity3d.com

스레드(Thread): 프로세스를 구성하는 작업단위 (CPU 시간을 할당하는 기본 단위)

프로세스(Process): 프로그램을 실행하는 과정 (스레드들의 모임: 하나 이상의 스레드로 구성, 프로그램)

 

한 프로세스 내에서 동시에 돌아가는 것들은 스레드이다.

 

단일 스레드 컴퓨팅 시스템 - 한번에 하나의 명령어가 입력->한번에 하나의 결과가 출력

(프로그램 로드~완료까지 시간은 CPU가 수행해야할 작업량에 따라 다르다)

멀티 스레딩 시스템 - 여러 코어에서 한번에 여러 개의 스레드를 처리하는 CPU 성능을 활용하는 프로그래밍의 유형이다. 따라서 동시에 여러 개의 작업 또는 명령을 실행

 

어떤 스레드는 기본적으로 프로그램이 시작할 때 실행된다. 이 스레드가 '메인 스레드'이다. 메인 스레드는 작업을 처리하기 위해 새로운 스레드를 생성하고, 이러한 새 스레드는 다른 스레드와 병렬로 실행되며 → 실행이 완료되면 메인 스레드와 결과를 동기화한다. [멀티 스레딩 동작방식]

 

멀티스레딩 방식은 여러 개의 작업이 오랫동안 실행되는 경우에 적합하다.

일반적으로 게임 개발 코드는 한 번에 실행해야 할 작은 명령들이 많이 들어있다. 이렇게 각 명령에 대해 스레드를 만들면 그 수가 너무 많아지며 각각의 수명도 짧아지므로 CPU 및 운영체제의 프로세싱 능력을 초과할 수 있다. [멀티 스레딩 필요이유]

 

1) 대화형 프로그램의 응답성 개선
2) 자원 공유가 쉽다.

3) 과다 사용은 오히려 성능 저하, 작업간 전환(Context Switching) 발생시킴.

 

Context Switching : 실행 도중에 스레드 상태를 저장하고 다른 스레드에 대한 작업을 진행한 후 첫번째 스레드를 재구성하여 나중에 계속 처리하는 프로세스

*CPU 코어보다 스레드 수가 더 많아서 CPU 리소스를 두고 스레드 간 경쟁이 벌어져 발생한다.

 

프레임(frame) : 게임 엔진이 화면을 갱신하는 최소 단위
*게임은 초 당 몇 프레임을 렌더링하느냐에 따라 화면이 부드럽게 보인다.


동기(Synchoronus: 동시에 일어나는) : 요청과 그 결과가 동시에 일어난다. 바로 요청을 하면 시간이 얼마나 걸리든 결과가 주어져야한다.

*순서에 맞춰 진행되는 장점이 있지만, 여러가지 요청을 동시에 처리할 수 없다. 설계가 간단하지만 결과가 주어질 때까지 대기해야한다.

비동기(Asynchronous: 동시에 일어나지 않는) : 요청과 결과가 동시에 일어나지 않는다. 하나의 요청에 따른 응답을 즉시 처리하지 않아도, 이때 대기 시간 동안 또 다른 요청에 대해 처리가능하다.

*여러 개의 요청을 동시에 처리할 수 있는 장점이 있으나 동기 방식보다 속도가 떨어질 수 있다. 설계가 비교적 복잡하지만 결과가 주어지는데 시간이 걸리더라도, 그 시간 동안 작업을 할 수 있으므로 자원을 효율적으로 사용할 수 있다.


https://docs.unity3d.com/kr/2022.3/Manual/Coroutines.html

 

코루틴 - Unity 매뉴얼

코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있습니다. Unity에서 코루틴은 실행을 일시 정지하고 제어를 Unity에 반환하지만 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드입니

docs.unity3d.com

코루틴(Coroutine)

코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있다.

Unity에서 코루틴은 '실행을 일시정지하고 Unity에 제어권을 돌려주고, 계속할 때는 중단한 부분에서 다음 프레임을 계속할 수 있는 메소드'이다.

 

대부분의 경우 메소드를 호출하면, 실행을 완료한 뒤 호출한 메소드에 제어와 선택적 반환 값을 반환한다. 즉 메소드 내에서 발생한 모든 행동은 단일 프레임 업데이트 내에서 발생해야한다.

시간의 흐름에 따른 이벤트의 시퀀스나 절차상의 애니메이션을 포함하기 위해 메서드 콜을 사용하고자 하는 상황에서 코루틴을 사용할 수 있다.

하지만 코루틴은 스레드가 아니라는 점을 명심해야한다. 코루티의 동기 작업은 여전히 메인 스레드에서 실행되고, 메인 스레드에 소요되는 CPU 시간을 줄이려면 다른 스크립트 코드에서와 마찬가지로 코루틴의 작업 차단을 방지하는 것이 중요하다.

 

Q. 코루틴의 작업 차단?
유니티의 단일 스레드 모델에 따라서 주로 메인 스레드에서 게임 로직을 처리하고, 렌더링을 담당한다. 따라서 코루틴도 메인 스레드에서 동작하게 된다. 코루틴이 메인 스레드에서 동작하는데 코루틴 내의 장기실행 loop(반복문)이나 대기시간이 긴 작업을 수행하게되면 메인 스레드가 차단(blocked) [:종료를 기다리느라 작업이 중단]될 수 있다. 메인 스레드가 차단되면 게임의 모든 로직이 멈추게 되어 사용자 경험에 악영향을 미칠 수 있다.
따라서 "코루틴의 작업 차단을 방지한다"는 것은 코루틴 내에서 메인 스레드를 blocked하지 않고 비동기적으로 작업을 처리하도록 하는 것을 의미한다.

 

→코루틴은 HTTP 전송, 에셋 로드, 파일 I/O 완료 등을 기다리는 것처럼 긴 비동기 작업을 처리해야하는 경우 코루틴을 사용하는 것이 가장 좋다.


코루틴 예제

void Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
    }
}

오브젝트의 알파(불투명도) 값을 다음과 같이 보이지 않을 때까지 점차 줄이는 작업

 

이 예제에서 Fade() 메소드는 기대했던 효과를 내지 못한다. 페이딩을 보이게 하려면 Unity가 렌더링하는 중간 값을 표시하기 위해 프레임의 시퀀스(알고리즘 내에서 공간적 시간적으로 정해져있는 순서)에 페이드 알파를 줄여야한다.
[=페이드 인/아웃 효과를 구현하려면, 일반적으로 알파값을 서서히 변경하여 오브젝트를 나타내거나/사라지게 만들어야한다. 이를 위해 각 프레임에서 알파 값을 조금씩 줄여가면서 투명도를 변경한다.

 

하지만 이 예제의 메소드는 단일 프레임 업데이트 내에서 전체를 실행한다. 중간값은 절대 표시되지 않으며, 오브젝트는 즉시 사라진다. 따라서 이 상황을 해결하려면 코드를 프레임 단위로 페이드를 실행하는 Update() 함수에 추가할 수 있다.

 

하지만 이런 종류의 작업에는 코루틴을 사용하는 것이 더 편리하다.

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return null;
    }
}

코루틴은 Ienumerator 반환타입과 body 어딘가에 포함된 yield 반환문으로 선언하는 메소드이다. yield return null 라인은 실행이 일시정지되고, 다음 프레임에서 다시 시작되는 지점이다.

void Update()
{
    if (Input.GetKeyDown("f"))
    {
        StartCoroutine(Fade());
    }
}

코루틴 실행을 설정하려면 다음과 같이 StartCoroutine() 함수를 사용해야한다.

Fade() 함수의 루프 카운터는 코루틴의 수명동안 올바른 값을 유지하며 다른 변수나 파라미터는 yield문 간에 보존된다.


코루틴 시간 지연

IEnumerator Fade()
{
    Color c = renderer.material.color;
    for (float alpha = 1f; alpha >= 0; alpha -= 0.1f)
    {
        c.a = alpha;
        renderer.material.color = c;
        yield return new WaitForSeconds(.1f);
    }
}

기본적으로 Unity는 yeild문 다음에 코루틴을 다시 시작한다. 시간 지연을 도입하려면 다음과같이 WaitForSeconds()를 사용한다.

WaitForSeconds()를 사용하여 한동안 효과를 전파할 수 있고 Update() 메소드 안의 작업을 포함하는 대신 사용할 수 있다. Unity는 초당 여러 번 Update() 메서드를 호출하며 작업을 자주 반복할 필요가 없는 경우 코루틴에 넣어 정기적으로 업데이트를 할 수는 있지만 모든 단일 프레임을 업데이트할 수는 없다.

bool ProximityCheck()
{
    for (int i = 0; i < enemies.Length; i++)
    {
        if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
                return true;
        }
    }

    return false;
}

예를들어 애플리케이션에 위 코드를 사용하여 적이 근처에 있을 경우 경고를 하는 알람을 넣을수도 있다.

적이 많은 경우 이 함수를 모든 프레임에 호출하면 상당한 오버헤드(overhead : 어떤 프로세스를 실행하는데 추가적인 부하나 비용/함수 호출이나 작업을 수행할 때 발생하는 추가적인 계산이나 리소스 사용)가 발생할 수 있다.

IEnumerator DoCheck()
{
    for(;;)
    {
        if (ProximityCheck())
        {
            // Perform some action here
        }
        yield return new WaitForSeconds(.1f);
    }
}

하지만 코루틴을 사용하여 10분의 1초마다 호출할 수 있다. 이렇게 하면 게임 플레이에 눈에띄는 영향 없이 수행되는 검사 수를 줄인다.


코루틴 정지

.StopCoroutine()과 .StopAllCoroutines()을 사용하여 코루틴을 정지할 수 있다. 코루틴에 연결된 게임 오브젝트를 비활성화하기 위해 SetActive()를 false로 설정하면 코루틴이 정지된다. Destroy(MonoBehaviour의 인스턴스)를 호출하면 OnDisable을 즉시 트리거하며 Unity는 코루틴을 처리하여 효과적으로 정지시킨다. 마지막으로 OnDestroy는 프레임 끝에서 호출된다.
*활성화에서 false로 설정하여 MonoBehaviour를 비활성화한 경우, 코루틴이 정지되지 않는다.

 

StopAllCoroutines()

using UnityEngine;
using System.Collections;

// Create two coroutines that run at different speeds.
// When the space key is pressed stop both of them.

public class ExampleClass : MonoBehaviour
{
    //coroutine 1
    IEnumerator DoSomething1()
    {
        while (true)
        {
            print("DoSomething1");
            yield return new WaitForSeconds(1.0f);
        }
    }

    //coroutine 2
    IEnumerator DoSomething2()
    {
        while (true)
        {
            print("DoSomething2");
            yield return new WaitForSeconds(1.5f);
        }
    }

    void Start()
    {
        StartCoroutine("DoSomething1");
        StartCoroutine("DoSomething2");
    }

    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            StopAllCoroutines();
            print("Stopped all Coroutines: " + Time.time);
        }
    }
}

StopAllCoroutines()은 0개 이상의 코루틴을 실행할 수 있는 MonoBehaviour의 모든 코루틴을 중지하는데 사용된다. 따라서 인수가 필요하지 않다.
*StopAllCoroutines()은 연결된 하나의 스크립트에서만 작동한다.

 

StopCoroutine()
public void StopCoroutine(string methodName);
public void StopCoroutine(IEnumerator routine);
public void StopCoroutine(Coroutine routine);
*StartCoroutine()에서 문자열이 인수로 사용되는 경우 이 문자열을 사용한다. (IEnumerator 사용도 마찬가지)

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour
{
    // keep a copy of the executing script
    private IEnumerator coroutine;

    // Use this for initialization
    void Start()
    {
        print("Starting " + Time.time);
        coroutine = WaitAndPrint(3.0f);
        StartCoroutine(coroutine);
        print("Done " + Time.time);
    }

    // print to the console every 3 seconds.
    // yield is causing WaitAndPrint to pause every 3 seconds
    public IEnumerator WaitAndPrint(float waitTime)
    {
        while (true)
        {
            yield return new WaitForSeconds(waitTime);
            print("WaitAndPrint " + Time.time);
        }
    }

    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            StopCoroutine(coroutine);
            print("Stopped " + Time.time);
        }
    }
}

아래 예제는 StopCoroutine(Coroutine)이 사용되는 예시를 보여준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ExampleClass : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(coroutineA());
    }

    IEnumerator coroutineA()
    {
        // wait for 1 second
        yield return new WaitForSeconds(1.0f);
        Debug.Log("coroutineA() started: " + Time.time);

        // wait for another 1 second and then create b
        yield return new WaitForSeconds(1.0f);
        Coroutine b = StartCoroutine(coroutineB());

        yield return new WaitForSeconds(2.0f);
        Debug.Log("coroutineA() finished " + Time.time);

        // B() was expected to run for 10 seconds
        // but was shut down here after 3.0f
        StopCoroutine(b);
        yield return null;
    }

    IEnumerator coroutineB()
    {
        float f = 0.0f;
        float start = Time.time;

        Debug.Log("coroutineB() started " + start);

        while (f < 10.0f)
        {
            Debug.Log("coroutineB(): " + f);
            yield return new WaitForSeconds(1.0f);
            f = f + 1.0f;
        }

        // Intended to handling exit of the this coroutine.
        // However coroutineA() shuts coroutineB() down. This
        // means the following lines are not called.
        float t = Time.time - start;
        Debug.Log("coroutineB() finished " + t);
        yield return null;
    }
}

코루틴 분석

출처: https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=pxkey&logNo=221296053953

 

 

유니티 코루틴(Coroutine)과 서브 루틴

안녕하세요. 창작자 픽케입니다. 우리가 어떤 작업을 처리하기 위해 필요한 함수(Function)를 호출하면, ...

blog.naver.com

어떤 작업을 처리하기 위해 함수(Function)를 호출하면, 그 함수는 로직을 모두 수행한 이후 자신을 호출한 메인 루틴(Main Routine) 속 코드의 다음 위치로 돌아가서 프로그램의 흐름이 이어지게 된다. 만약 서브루틴(Sub Routine)이 반환(Return)하는 값이 있다면, 메인 루틴은 이 값을 사용할 수도 있다. 이것을 일반적으로 서브루틴이라고 표현한다.

 

 서브 루틴이 메인 루틴에 종속된 상태로 호출을 받는 입장이라고 한다면, 코루틴(Coroutine)코루틴을 호출한 메인루틴과 함께 실행되는 구조이다. 따라서 메인 루틴과는 대등한 관계라고 할 수 있다.

IEnumerator TestCoroutine() {
	///수행할 작업
    yield return null;
}

yield함수의 코드가 실행한 결과를 반환한다. 코루틴이 함수에 정의된 작업을 실행하다가 yield return 구문을 만나면 yield return 구문으로 지정한 명령을 실행시키고, 코루틴을 호출했던 루틴(함수블럭?)으로 잠시 제어의 권한을 넘겨준다.  이는 코루틴의 실행이 일시적으로 중지되는 것이다.

위 코드에서 yield return이 지정한 "null"은 1프레임을 코루틴의 호출자에게 양보하고 대기하라는 뜻이다. 즉 다음 프레임까지 대기를 해야할 경우 사용할 수 있다. 만약 예를들어 3초동안 대기했다가 다음 작업을 실행하려면 yield return 구문을 아래와 같이 지정할 수 있다.

yield return WaitForSeconds(3.0f);

3초가 지나면 프로그램의 제어권은 코루틴 함수에서 yield return으로 중단되었던 코드의 위치로 복귀하여 코루틴 함수의 작업을 계속 이어간다. yield 구문으로 반환되는 값이 코루틴 함수가 다시 실행(재개)되는 '시점'을 지정하는 것이다. 이것이 서브루틴과 코루틴의 결정적인 차이이다.

서브 루틴은 호출될 때마다 코드의 첫 부분부터 다시 실행하지만, 코루틴은 중단되었던 코드의 다음 위치부터 실행된다. 함수가 시작되는 위치를 진입 지점(Entry Point)라고 하는데, 진입 지점이 1개인 서브 루틴에 비해서, 코루틴은 진입 지점을 여러 개 정의할 수 있다.
아래는 코루틴에서 사용하는 반환타입의 목록이다. (코루틴이 코루틴의 호출자에게 양보하는 조건이라고 생각)

null 다음 프레임까지 대기
new WaitForSeconds(float) 지정한 초만큼 대기
new WaitForFixedUpdate() 다음 물리 프레임까지 대기(FixedUpdate())
new WaitForEndOfFrame() 모든 렌더링 작업이 끝날 때까지 대기
StartCoroutine(string) 다른 코루틴이 끝날때까지 대기
new WWWW(string) 웹 통신 작업이 끝날 때까지 대기
new AsyncOperatino 비동기 작업이 끝날때까지 대기

코루틴은 StartCoroutine() 함수의 매개변수로 '함수'를 전달하여 호출할 수 있다.

StartCoroutine(TestCoroutine());

그리고 코루틴 함수의 이름을 문자열 형식으로 전달할 수도 있다.

StartCoroutine("TestCoroutine");

그러나 코루틴 함수의 이름을 문자열 형식으로 전달하면, 간편하게 사용할 수는 있을 것이지만 실제 메소드를 찾아내는 과정(=연산)이 필요하기 때문에 성능이 저하될 수 있다. 
*또한 StopCoroutine()을 호출하여 조기중단시킬 수 있다.

만약 코루틴의 매개변수가 1개 있을 경우에는 아래와 같이 호출할 수도 있다.

StartCoroutine("TestCoroutine", 2.0f);

코루틴과 서브 루틴의 차이점을 확인할 수 있는 가장 간단한 예제 코드는 아래와 같다.

void start() {
	StartCoroutine(TestCo());
	//StartCoroutine("TestCo");
	TestSub();
}

IEnumerator TestCo() {
	Debug.Log("[co] 1");
	yield return new WaitForSeconds(1);
	Debug.Log("[co] 2");
	Debug.Log("[co] 3");
}

public void TestSub() {
	for (int i = 0; i < 3; i++)
		Debug.Log("[Sub] " + i);
}

위 코드의 실행 결과는 아래와 같다. 코루틴의 yield return 구문을 통해 1프레임을 대기하는 동안 프로그램의 제어권한이 코루틴의 호출자인 Start() 함수로 넘어왔다. 그래서 서브루틴인 TestSub()가 실행되어 작업을 모두 처리한 후, 다시 제어 권한이 코루틴으로 넘어가서 yield return으로 중단된 그 이후의 나머지 작업을 실행한 것을 확인할 수 있다.

코루틴은 하나의 프로세스를 여러 루틴들이 시간을 나눠서 사용하는 방식이다. 그러나 스레드(Thread)는 동시에 여러 프로세스가 여러 작업을 진행하는 것이다.

코루틴을 통해 개발자는 여러 프레임에 걸쳐 일어나는 동작들을 덜 복잡하게 설계할 수 있다. 

 

네이버 사전에서 'Coroutine'을 검색해보면 "각 호출에서 초기화되는 서브루틴과는 달리, 호출 시 관련된 모든 정보를 보존하는 능력을 갖는다. 그리고 다음에 다시 시작할 때에는 이전에 실행했던 다음부터 실행할 수 있는 논리를 갖는다.

 

더욱 자세한 내용은 아래 영상을 참고해보도록 한다.

https://youtu.be/bM3CXzj5xM4?feature=shared

 

IEnumerator 타입의 함수는 yield return으로 반환하고 (코루틴) StartCoroutine()으로만 호출할 수 있다. 만약 return을 사용하려면 예를들어 FireForward()함수 내에 특정 조건을 만족하지 못하면 return을 시키는 로직을 작성하고, 이 함수가 다시 n초 후 반복실행되도록 하려면 별도의 FireForwardWithDelay(float delay) 코루틴 함수를 만들어서 함수 로직으로 yield return new WaitForSeconds(delay); 그리고 FireForward();로 함수를 호출하는 식으로 작성해야한다.

 

추가: 일반적인 void 반환타입의 함수와 같은데, 여기서 '다음 프레임까지 대기', 혹은 '몇 초간 대기' 등의 시간을 멈추고 지연시키는 기능이 추가된 것이 코루틴 함수라고 이해하면 될 것 같다.

Comments