본문 바로가기
유니티

Unity Thread, Task, async/await, EventBus

by 유니티세상 2026. 4. 28.
반응형

1. Unity와 메인 스레드

Unity는 대부분의 API가 thread-safe하지 않다.
따라서 Unity 객체를 다루는 코드는 반드시 메인 스레드에서 실행해야 한다.

메인 스레드에서만 안전한 작업

  • GameObject 생성/삭제
  • Transform 변경
  • UI 변경 (Text, Image 등)
  • Animator 조작
  • Scene 관련 작업
  • MonoBehaviour 접근
  • UnityEngine.Object 관련 작업

백그라운드 스레드에서 가능한 작업

  • 파일 읽기
  • JSON 파싱
  • 네트워크 요청 처리
  • 데이터 가공
  • AI 계산
  • 경로 탐색
  • 암호화/복호화
  • 순수 C# 로직

2. EventBusManager에서 Thread 체크를 하는 이유

private readonly int _mainThreadId = Thread.CurrentThread.ManagedThreadId;

 

EventManager가 생성된 시점의 스레드 ID를 저장한다.
이 값은 이후 메인 스레드 판별 기준으로 사용된다.

 

public void Notify<T>(T eventData) where T : IEvent
{
    if (Thread.CurrentThread.ManagedThreadId != _mainThreadId)
    {
        Debug.LogError("Main Thread에서만 Notify 가능");
        return;
    }

    // handler 실행
}

핵심 이유

Notify는 단순한 알림 함수가 아니라
등록된 handler들을 즉시 실행하는 함수다.

즉 다음과 같은 구조가 된다.

Notify 호출
→ 구독된 handler 실행
→ handler 내부에서 UI, GameObject 접근 가능

 

따라서 Notify가 백그라운드 스레드에서 호출되면 handler도 백그라운드 스레드에서 실행된다.

그 결과 Unity API를 잘못된 스레드에서 호출하게 되어 오류, 크래시, 비정상 동작이 발생할 수 있다.


3. Task.Run과 백그라운드 스레드

Task.Run(() =>
{
    LoadData();
    eventManager.Notify(new DataLoadedEvent());
});

Task.Run은 작업을 ThreadPool의 작업자 스레드에서 실행한다.
즉 메인 스레드가 아닌 백그라운드 스레드에서 실행된다.

문제는 이 코드에서 Notify가 호출된다는 점이다.

백그라운드 스레드
→ Notify 호출
→ handler 실행
→ UI 접근
→ 문제 발생

4. 왜 Notify를 막는가

EventBusManager는 다음 상황을 방지하기 위해 방어 코드를 둔다.

백그라운드 스레드에서 Notify 호출
→ handler도 백그라운드에서 실행
→ Unity API 접근
→ 위험

따라서 구조적으로 다음 규칙을 강제한다.

Notify는 반드시 메인 스레드에서만 호출

5. async/await에서 주의할 점

async void LoadData()
{
    var data = await HttpClient.GetAsync(...);

    eventManager.Notify(new DataLoadedEvent(data));
}

await는 다음을 보장한다.

비동기 작업이 끝난 뒤 아래 코드가 실행된다

하지만 다음은 보장하지 않는다.

await 이후 코드가 반드시 메인 스레드에서 실행된다

이유는 다음과 같다.

  • SynchronizationContext에 따라 실행 위치가 달라질 수 있음
  • ConfigureAwait(false)를 사용하면 메인 스레드로 돌아오지 않음
  • Task.Run과 함께 사용하면 백그라운드에서 이어질 수 있음

정리하면:

await는 실행 순서는 보장하지만
실행 스레드는 항상 보장하지 않는다

 

https://www.sysnet.pe.kr/2/0/13191

 

.NET Framework: 2077. C# - 직접 만들어 보는 SynchronizationContext

.NET Framework: 2077. C# - 직접 만들어 보는 SynchronizationContext [링크 복사], [링크+제목 복사], 조회: 25743 글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 [sync_ctx_user_sample.zip]     부모글 보

www.sysnet.pe.kr

 


6. 안전한 구조 (큐를 통한 메인 스레드 처리)

백그라운드에서 바로 Notify하지 않고,
메인 스레드에서 처리하도록 넘기는 구조를 사용한다.

private readonly Queue<IEvent> pendingEvents = new Queue<IEvent>();

public void EnqueueNotify(IEvent eventData)
{
    pendingEvents.Enqueue(eventData);
}

private void Update()
{
    while (pendingEvents.Count > 0)
    {
        var eventData = pendingEvents.Dequeue();
        Notify(eventData);
    }
}

흐름

백그라운드 스레드
→ 데이터 처리
→ EnqueueNotify 호출

메인 스레드 (Update)
→ 큐에서 이벤트 꺼냄
→ Notify 실행
→ handler 실행 (안전)

멀티스레드 환경에서는 Queue 대신 ConcurrentQueue 사용이 권장된다.


7. Coroutine과 Thread의 차이

Coroutine은 멀티스레드가 아니다.
메인 스레드에서 실행되는 분할 실행 구조다.

Coroutine 특징

  • 메인 스레드에서 실행
  • yield를 기준으로 실행이 나뉨
  • 여러 개가 동시에 도는 것처럼 보이지만 실제로는 순차 실행

Thread / Task와 차이

구분 실제 스레드 Unity API 안전
Coroutine X O
Task / Thread O X

8. 정리

  • Unity API는 메인 스레드에서만 안전하다
  • Notify는 handler를 즉시 실행하므로 호출 스레드가 중요하다
  • Task.Run은 백그라운드 스레드에서 실행된다
  • await는 순서를 보장하지만 스레드를 항상 보장하지 않는다
  • 따라서 Notify는 메인 스레드에서만 실행되도록 강제해야 한다
  • 백그라운드 결과는 큐를 통해 메인 스레드에서 처리하는 구조가 안전하다

 

반응형

'유니티' 카테고리의 다른 글

c# 심화  (0) 2026.04.28
유니티 생성자, 소멸자  (0) 2026.04.28
unity TMP_TextUtilities 클릭한 글자 가져오기  (0) 2026.04.23
TextMeshPro(TMP) <link></link>, unity 텍스트 하이퍼링크  (0) 2026.04.23
AES-256 이란?  (0) 2026.04.01