로그인게임하기
Unity C# 강좌

[Unity C# 7] 코루틴과 스레드, 기다리는 두 가지 방법

2026.10.14

Unity C# 강좌 · 7편

3초 뒤에 메시지를 띄우고 싶을 때, 함수 안에서 3초를 기다리면 게임 전체가 3초 멈춥니다. Unity 는 이 문제를 코루틴으로 풀고, 운영체제는 스레드로 풉니다. 이번 편에서는 같은 "기다렸다가 찍기" 를 두 방법으로 만들어 차이를 봅니다.

수업 정보

2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_07_Coroutine.cs 로 가르친 내용입니다. 수업은 두 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.

PROBLEM

기다리면 게임이 멈춘다

Unity 는 한 프레임 안에서 모든 스크립트의 Update 를 차례로 부르고, 그다음에 화면을 그립니다. 누군가 함수 안에서 3초를 기다리면, 그동안 다음 스크립트도 화면도 기다립니다.

메인 스레드 한 프레임Update() 들 → 코루틴 이어 가기 → 화면 그리기
Thread.Sleep(3000) 을 넣으면이 프레임이 3초 동안 끝나지 않는다 → 화면이 멈춤
STEP 1

코루틴: 멈췄다가 다음 프레임에 이어 가기

Strategy_07_Coroutine.cs수업 자료 1단계8–29줄
private IEnumerator DelayedPrint(float delay, string title, string someText)
{
    float time = 0;

    while (time < delay)
    {
        time += 1;
        yield return new WaitForSeconds(1);

        Program.WriteLine("{0}: {1}", title, time);
    }

    Program.WriteLine("{0}: {1}", title, someText);

    yield return null;
}

public void Run()
{
    Program.Get().StartCoroutine(DelayedPrint(3.0f, "첫번째 코루틴", "3초가 지났습니다."));
    Program.Get().StartCoroutine(DelayedPrint(5.0f, "두번째 코루틴", "5초가 지났습니다."));
}

6편의 yield 가 여기서 쓰입니다. DelayedPrint 는 IEnumerator 를 돌려주는 함수입니다. StartCoroutine 에 넘기면, Unity 가 매 프레임 이 IEnumerator 의 MoveNext 를 대신 불러 줍니다.

yield return 값Unity 가 다음 MoveNext 를 부르는 때
null다음 프레임
new WaitForSeconds(1)1초가 지난 뒤의 프레임
new WaitUntil(() => 조건)조건이 참이 된 프레임
실행 결과 (1초마다)게임 화면의 출력 창
첫번째 코루틴: 1
두번째 코루틴: 1
첫번째 코루틴: 2
두번째 코루틴: 2
첫번째 코루틴: 3
첫번째 코루틴: 3초가 지났습니다.
두번째 코루틴: 3
두번째 코루틴: 4
두번째 코루틴: 5
두번째 코루틴: 5초가 지났습니다.

두 코루틴이 번갈아 찍히니 동시에 도는 것처럼 보입니다. 하지만 둘 다 메인 스레드 하나에서 차례로 이어 갈 뿐입니다. 그래서 코루틴 안에서는 Program.WriteLine 처럼 Unity 화면을 고치는 코드를 마음껏 쓸 수 있습니다.

StartCoroutine 은 MonoBehaviour 에게

Strategy 는 MonoBehaviour 가 아니라 코루틴을 직접 시작할 수 없습니다. 그래서 씬에 있는 Program(MonoBehaviour)에게 Program.Get().StartCoroutine(…) 으로 부탁합니다.

STEP 2

스레드: 진짜로 따로 도는 흐름

Strategy_07_Coroutine.cs수업 자료 2단계8–42줄
private void DelayedPrint(float delay, string title, string someText)
{
    double time = 0;
    DateTime lastTime = DateTime.Now;

    while (time < delay * 1000)
    {
        TimeSpan ts = DateTime.Now - lastTime;
        time += ts.TotalMilliseconds;

        Debug.LogFormat("{0}: {1}", title, Mathf.FloorToInt((float)time / 1000.0f));

        lastTime = DateTime.Now;
        Thread.Sleep(1000);
    }

    Debug.LogFormat("{0}: {1}", title, someText);
}

public void Run()
{
    //Program.Get(); // 메인 쓰레드에서 미리 싱글톤 인스턴스를 찾아놓는다.

    Thread firstThread = new Thread(() => DelayedPrint(3.0f, "첫번째 코루틴", "3초가 지났습니다."));

    Thread secondThread = new Thread(new ThreadStart(
        delegate
        {
            DelayedPrint(5.0f, "두번째 코루틴", "5초가 지났습니다.");
        }
    ));

    firstThread.Start();
    secondThread.Start();
}

2단계에서는 같은 일을 Thread 두 개로 합니다. 각 스레드는 운영체제가 따로 돌리는 흐름이라, 안에서 Thread.Sleep(1000) 으로 1초를 쉬어도 게임 화면은 멈추지 않습니다. 스레드를 만드는 두 가지 방법(람다를 바로 넘기기, ThreadStart 델리게이트로 감싸기)도 4편에서 본 문법 그대로입니다.

왜 Debug.LogFormat 인가

스레드 안에서는 Program.WriteLine 대신 Debug.LogFormat 을 썼습니다. Unity 의 오브젝트와 컴포넌트(글 상자 포함)는 메인 스레드에서만 만질 수 있기 때문입니다. 주석의 "메인 쓰레드에서 미리 싱글톤 인스턴스를 찾아놓는다" 도 같은 이유입니다. FindObjectOfType 같은 Unity API 는 다른 스레드에서 부르면 오류가 납니다.

LOCK

두 스레드가 같은 목록을 만질 때

원본 소스의 2단계에는 두 스레드가 같은 리스트에 값을 넣고 읽는 코드가 더 있습니다.

Strategy_07_Coroutine.csAssets/Source/Strategy_07_Coroutine.cs37–38줄필드
private List<int> intarr = new List<int>();
private object lockArr = new object();
Strategy_07_Coroutine.csAssets/Source/Strategy_07_Coroutine.cs56–65줄
lock (lockArr)
{ // Critical Section
    intarr.Add(value);
    value++;

    foreach (var item in intarr)
    {
        Debug.Log(item.ToString());
    }
}

한 스레드가 리스트를 foreach 로 돌고 있는데 다른 스레드가 Add 를 하면, 리스트가 도중에 바뀌어 예외가 나거나 값이 꼬입니다. lock (lockArr) 은 "이 중괄호 안에는 한 번에 한 스레드만 들어온다" 는 표시입니다. 이렇게 보호하는 구간을 임계 구역(critical section)이라고 합니다.

코루틴

  • 메인 스레드 하나에서 프레임마다 이어 간다
  • Unity API 를 마음껏 쓴다
  • lock 이 필요 없다
  • 무거운 계산을 하면 그 프레임이 느려진다

스레드

  • 운영체제가 따로 돌린다
  • Unity API 를 쓸 수 없다
  • 같은 데이터를 만지면 lock 이 필요하다
  • 무거운 계산이나 네트워크 대기에 알맞다
RETRO

돌아보며

이번 편에서 얻은 것

  • 코루틴은 IEnumerator 를 Unity 가 프레임마다 이어 가 주는 것이다
  • 스레드는 진짜로 따로 돌지만 Unity API 를 쓸 수 없다
  • 여러 스레드가 같은 데이터를 쓰면 lock 으로 보호한다

아쉬운 점

  • 스레드 결과를 화면에 띄우려면 메인 스레드로 넘기는 장치(큐 등)가 따로 필요한데, 이 수업에서는 다루지 않는다

정리

  1. 게임 안에서 기다릴 때는 코루틴입니다. yield return new WaitForSeconds(1) 은 1초 뒤 프레임에 이어 갑니다.
  2. 스레드는 메인 스레드 밖에서 돕니다. 화면을 멈추지 않지만 Unity 오브젝트를 만질 수 없습니다.
  3. 공유 데이터는 lock 으로 지킵니다. 한 번에 한 스레드만 들어오게 해서 목록이 도중에 바뀌지 않게 합니다.

다음 편에서는 var 와 초기화 문법, 그리고 프로퍼티로 선언을 짧고 안전하게 만드는 법을 봅니다.

← 개발 노트 목록