티스토리 뷰
코루틴과 쓰레드
닷넷은 멀티쓰레드를 지원하지만 닷넷을 사용하는 유니티는 단일 쓰레드로 동작한다. 멀티쓰레드를 가지는 코드르 작성함으로써 버그 발생률이 높아지고 멀티쓰레드간 교착 상태 경합 등, 신경써야 할 부분이 많아진다. 그래서 깔끔하게 단일 쓰레드만 사용하는 것에 찬성한다. 그렇다면 쓰레드를 사용하지 않고 어떻게 멀티 태스킹을 구현할 수 있을까? 이것을 해결해 주는 것이 유니티 코루틴(Coroutine)이다. 일반적인 쓰레드 방식의 프로그래밍을 유니티에서는 코루틴을 사용함으로써 멀티 쓰레드를 흉내 낼 수 있다.
코루틴의 동작방식
쓰레드를 사용하지 않고 멀티 테스킹을 가능하게 한다. 어떻게 이것이 가능 할 수 있을까? 코루틴 함수가 IEnumerator 를 반환하는데 힌트를 얻을 수있다. C#에서 이터레이터는 프로그래머가 IEnumerator 를 구현할 수있도록 돕는 기능이다. 열거형의 MoveNext() 를호출 할 때마다. 해당하는 다음 값을 반환한다. 일반적으로 함수가 호출되서 끝나는 서브루틴의 개념과 달리 yield return 을 사용함으로써 그 위치를 기억하고 다음 호출 때 그곳부터 다음을 실핼 할 수있도록 하는 것이다. 그래서 여러개의 코루틴을 동작시키고 각기다른 시점에 yield가 반환되도록 한다면 마치 어러개의 쓰래드가 동시에 동작하는것과 같은 효과가 나타난다. 유니티의 Update 함수는 1프래임에 한 번 호출 된다. 매 프래임마다 yield return 할 코루틴이 있는지 체크하는데 코루틴의 yield 종류마다 그 시점이 다르다. 대부분은 Update 에서 체크된다.
(유니티 메뉴얼 참고)
yield 코루틴은 다음 프레임에서 모든 Update 함수가 호출된 후에 속행합니다.
yield WaitForSeconds 프레임에 대한 모든 Update 함수가 호출된 후 지정된 시간 지연 후에 속행합니다.
yield WaitForFixedUpdate 모든 스크립트에서 모든 FixedUpdate 호출 후 속행합니다.
yield WWW WWW 다운로드 완료 후 속행합니다.
yield StartCoroutine 코루틴을 연결하고 MyFunc 코루틴이 먼저 완료될 때까지 기다립니다.
Coroutine 과 Invoke 비교
Invoke는 함수를 원하는 시점에 호출할 수있도록 해주는 함수이다. InvokeRepeating를 사용하여 함수를 반복적으로 호출 할 수도있다. 하지만 Invoke는 파라미터를 전달할 수없다. Invoke는 내부적으로 타이머에 의해 해당 시간이 되었는지 체크하여 시간이 되면 호출하는 방식이고 코루틴은 매 프래임마다 체크하여 돌아갈 지점으로 돌아가는 방식이다.
코루틴 사용
다음은 1초마다 로그를 찍는 함수를 코루틴으로 간단하게 구현한것이다.
//1초마다 로그를 찍는다 private IEnumerator DisplayLog() { while (true) { yield return new WaitForSeconds(1); Debug.Log("This is log! yay"); } }
위의 함수를 StartCoroutine(DisplayLog()); 로 호출하면 된다. InvokeRepeating을 사용하여도 같은 효과를 구현할 수있다. 코루틴을 사용하지 않았다면 Update 함수에서 시간을 체크해가며 로그를 찍도록 하였을 것이다. 코드도 길어지고 가독성도 떨어졌을 것이다. 하지만 코루틴은 위처럼 간결한 코드로 구현이 가능하다.
그런데 new 연사자를 루프안에서 자꾸 사용하는게 거슬린다. 가비지 컬렉터가 좋아하지 않는 코드이다.
해결책
WaitForSeconds waitSec = new WaitForSeconds(1); 를 미리 만들어 놓고 루프안에서 재활용하였다.
//1초마다 로그를 찍겠다 private IEnumerator DisplayLog() { WaitForSeconds waitSec = new WaitForSeconds(1); while (true) { yield return waitSec; Debug.Log("TEST!!"); } }
코루틴 중지(StopCoroutine)
코루틴 함수를 변수처럼 선언해 두고 필요할 때 시작,중지를 할 수있다.
using UnityEngine; using System.Collections; public class CoroutineTest : MonoBehaviour { Coroutine logCoroutine = null; //코루틴 변수 void Start () { //로그 출력 코루틴 실행 logCoroutine = StartCoroutine(DisplayLog()); //로그 출력 중단 코루틴 실행 StartCoroutine(StopDisplayLog()); } //1초마다 로그를 찍겠다 private IEnumerator DisplayLog() { WaitForSeconds waitSec = new WaitForSeconds(1); while (true) { yield return waitSec; Debug.Log("TEST!!"); } } //로그 출력 중단 private IEnumerator StopDisplayLog() { yield return new WaitForSeconds(3); //3초 뒤 코루틴 중단 StopCoroutine(logCoroutine); } }
로그를 찍는 코루틴을 실행시켜 1초마다 로그를 찍게했다. 동시에 로그를 3초 뒤 중단시키는 코루틴을 실행하여
StopCoroutine로 코루틴을 중단시켰다. or
루프를 카운트하여 while 문의 조건문을 체크하여루프를 빠저나가도 코루틴은 종료된다. or
while 루프를 break;로 빠져나가도 종료된다. or
while 루프안에서 yield break;로도 코루틴을 벚어날 수있다. or
StartCoroutine을 호출하는 스크립트가 붙은 오브젝트가 비활성화 되거나 파괴되면 코루틴은 중단된다.
코루틴 in 코루틴
코루틴안에서 또다른 코루틴을 실행하여 그것의 실행이 모두 끝나기까지 대기 후 다음을 진행할 수있다.
위 그림에서 Funciton0은 로그 Hello world 0을 출력 후 코루틴으로 Function2를 호출 후 다시 로그로 Hello world 1을 출력하였다. Fuction1에서는 yield return null 을 이용하여 한 프래임을 대기하고 Hello world 1 로그를 출력하게된다. 결국 로그 출력 순서는 Hello world 0, Hello world 1,Hello world 2 순서로 출력하게 된다. 하지만 로그를 Hello world 0, Hello world 2,Hello world 1 순서로 출력하고 싶다면 어떻게 해야할까? Function0 에서 Funciton1의 호출 후 Function1의 작업이 모두 끝난 후 Fuciton0의 다음 작업을 진행 하려면?
Fuciton0을 IEnumerator를 반환하도록 하고 Fuciton0에서 Fuciton1을 호출 할 때 yield return으로 코루틴을 실행하였다. 이렇게 하면 의도하던데로 Hello world 0, Hello world 2,Hello world 1순서로 로그가 출력되게 된다. 코루틴의 특성인 돌아갈 위치를 기억하고 돌아가는 기능이 있기 때문이다.
※ 코루틴 사용 주의사항
1. 코루틴 함수는 일반함수처럼 호출하면 실행되지 않는다. 그래도 에러는 커녕 워닝조차 뜨지않는다. 반드시 StartCoroutine 을 사용해 호출해야한다.
2. 코루틴은 Time scale의 영향을 받는다. 예를 들어 Time scale이 1인상태에서 WaitForSeconds(1)을 하였다면 1초를 대기하지만 Time scale 값이 0.5일 때에는 2초 대기를 하게된다.
3. StartCoroutine을 호출하는 스크립트가 붙은 오브젝트가 비활성화 되거나 파괴되면 코루틴은 중단된다.