비동기 프로그래밍

1. 개요

서버를 공부하면서 멀티스레드와 유니티에서의 비동기의 관계가 헷갈려서 정리했었던 내용의 일부이다.

최근에 같이 공부하던 친구가 궁금하다고 해서 유니티부분만 정리해 줬는데, 그 내용을 공유한다.

 

 

2. 기본 개념

2.1 동기 vs 비동기


동기(Synchronous):

작업이 순차적으로 실행되며, 각 작업은 이전 작업이 완료될 때까지 기다린다.

 

  장점: 

    - 코드의 흐름이 예측 가능하고 디버깅이 쉽다.

    - 데이터 일관성을 유지하기 쉽다.

 

  단점: 

    - I/O 작업 등으로 인한 대기 시간 동안 리소스가 낭비될 수 있다.

    - 사용자 인터페이스가 응답하지 않을 수 있다.

 

비동기(Asynchronous):

작업이 병렬적으로 실행되며, 한 작업의 완료를 기다리지 않고 다음 작업을 시작할 수 있다.

 

  장점: 

    - I/O 바운드 작업에서 효율적이며, UI의 응답성을 향상시킬 수 있다.

    - 리소스 사용을 최적화할 수 있다.

 

  단점: 

    - 코드의 흐름이 복잡해질 수 있으며, race condition 등의 동시성 문제가 발생할 수 있다.

    - 디버깅이 더 어려울 수 있다.

 

2.2 블로킹 vs 논블로킹

블로킹(Blocking):

작업이 완료될 때까지 프로그램의 실행을 멈추고 기다린다.

 

  장점: 

    - 코드가 직관적이고 이해하기 쉽다.

    - 순차적 실행이 필요한 작업에 적합

 

  단점: 

    - 블로킹 시간이 길어지면 리소스 사용이 비효율적이다.

    - 대기 시간 동안 다른 작업을 수행할 수 없다.

 

논블로킹(Non-blocking):

작업의 완료를 기다리지 않고 프로그램이 계속 실행된다.

 

  장점: 

    - 리소스를 효율적으로 사용할 수 있다.

    - 여러 작업을 동시에 처리할 수 있다.

 

  단점: 

    - 코드의 복잡성이 증가할 수 있다.

    - 작업 완료 시점을 관리하는 것이 더 어려울 수 있다.

 

2.3 병렬성 vs 동시성

병렬성(Parallelism):

여러 작업이 실제로 동시에 실행된다.(멀티코어 환경에서)

 

  장점: 

    - CPU 바운드 작업에서 성능을 크게 향상시킬 수 있다.

    - 복잡한 계산 작업을 빠르게 처리할 수 있다.

 

  단점: 

    - 데이터 레이스와 같은 동시성 문제를 주의해야 한다.

    - 모든 하드웨어에서 동일한 성능 향상을 기대할 수 없다.

 

동시성(Concurrency):

여러 작업이 동시에 진행되는 것처럼 보이지만, 실제로는 빠르게 전환되며 실행된다.

 

  장점: 

    - 단일 코어에서도 효율적인 작업 처리가 가능하다.

    - I/O 바운드 작업에 특히 효과적이다.

 

  단점: 

    - 실제 병렬 실행에 비해 성능 향상이 제한적일 수 있다.

    - 작업 전환 오버헤드가 발생할 수 있다.

 

 

3. C#(유니티)에서의 비동기 프로그래밍

3.1 Task와 Task<T>

Task: 비동기 작업을 나타내는 객체

Task<T>: 결과를 반환하는 비동기 작업

 

ex)

using System;
using System.Threading.Tasks;

public class TaskEx : MonoBehaviour
{
    static async Task Main()
    {
        Console.WriteLine("작업 실행");
        int result = await LongOperation();
        Console.WriteLine($"결과: {result}");
    }
    
    static async Task<int> LongOperation()
    {
        await Task.Delay(2000); // 시간이 걸리는 작업
        return 0;
    }
}

 

3.2 async와 await 키워드

async: 메서드가 비동기적으로 실행될 수 있음을 나타냄

await: 비동기 작업의 결과를 기다림

 

ex)

using UnityEngine;
using System.Threading.Tasks;

public class AsyncEx : MonoBehaviour
{
    async void Start()
    {
        Debug.Log("작업 실행");
        int result = await LongOperation();
        Debug.Log($"결과: {result}");
    }
    
    async Task<int> LongOperation()
    {
        await Task.Delay(2000); // 시간이 걸리는 작업
        return 0;
    }
}

 

3.3 코루틴(Coroutine)

유니티에서 제공하는 비동기 프로그래밍 방식

 

ex)

using UnityEngine;
using System.Collections;

public class CoroutineEx : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(LongOperation());
    }

    IEnumerator LongOperation()
    {
        Debug.Log("작업 실행");
        yield return new WaitForSeconds(2);
        Debug.Log("작업 종료");
    }
}

 

 

4. 코루틴

4.1. 기본개념

- 코루틴은 비동기적으로 동작하지만, 실제로는 메인 스레드에서 순차적으로 실행된다.

- 일정 시간 동안 실행을 일시 중지하고 나중에 재개할 수 있다.

- 보통 프레임 단위로 실행되며, 각 yield 지점에서 다음 프레임까지 실행을 일시 중지한다.

 

4.2. 동작 예시

시간 단위 실행:

IEnumerator MyCoroutine()
{
    Debug.Log("코루틴 시작");
    yield return new WaitForSeconds(1f);
    Debug.Log("1초 후");
}

 

프레임 단위 실행:

IEnumerator MyCoroutine()
{
    for (int i = 0; i < 5; i++)
    {
        Debug.Log($"프레임 {Time.frameCount}: 카운트 {i}");
        yield return null; // 다음 프레임까지 대기
    }
}

 

여러 코루틴의 동시 실행:

void Start()
{
    StartCoroutine(Coroutine1());
    StartCoroutine(Coroutine2());
}

IEnumerator Coroutine1()
{
    while (true)
    {
        Debug.Log("Coroutine1");
        yield return new WaitForSeconds(1f);
    }
}

IEnumerator Coroutine2()
{
    while (true)
    {
        Debug.Log("Coroutine2");
        yield return new WaitForSeconds(0.5f);
    }
}

두 코루틴이 동시에 실행되는 것처럼 보이지만, 실제로는 메인 스레드에서 번갈아가며 실행

 

 

5. async/await와 코루틴 비교

5.1. 차이점 및 장단점

실행 모델:

  - async / await: 비동기 작업이 완료될 때까지 현재 함수의 실행을 일시 중단

  - 코루틴: 실행을 일시 중지하고 나중에 재개

 

언어/프레임워크 지원:

  - async / await: C#의 언어 기능으로 .NET Framework에서 사용

  - 코루틴: 유니티 엔진에서 제공하는 기능으로, MonoBehaviour를 상속받은 클래스에서 사용 가능.

 

예외 처리:

  - async / await: try-catch 블록을 사용하여 예외를 직접 처리 가능

  - 코루틴: 예외 처리가 복잡하며, 직접 처리 메커니즘을 구현해야 함

 

취소:

  - async / await: CancellationToken을 사용하여 작업을 취소 가능

  - 코루틴: StopCoroutine을 사용하여 코루틴을 중지할 수 있지만, 세밀한 제어가 어려움

 

성능:

  - async / await: 일반적으로 더 높은 오버헤드를 가지지만, 복잡한 비동기 로직을 처리하는 데 유리.

  - 코루틴: 상대적으로 낮은 오버헤드를 가지며, 간단한 비동기 작업에 효율적

 

5.2. 실제 사용 시나리오

코루틴 사용이 적합한 경우:

1) 프레임 단위로 실행되어야 하는 작업

2) 유니티의 시간 기반 기능(예: WaitForSeconds)을 사용해야 하는 경우

3) 간단한 비동기 작업(예: 애니메이션, 타이머)

4) 유니티 API와 밀접하게 연동되는 작업

 

async/await 사용이 적합한 경우:

1. 복잡한 비동기 로직이 필요한 경우

2. 외부 API 호출이나 네트워크 작업

3. 파일 I/O 작업

4. 긴 시간이 걸리는 계산 작업

 

 

6. 코루틴 vs 멀티스레딩

동시성 vs 병렬성:

  - 코루틴: 동시성(Concurrency)을 제공. 작업들이 번갈아가며 실행되어 동시에 실행되는 것처럼 보임

  - 멀티스레딩: 병렬성(Parallelism)을 제공. 작업들이 실제로 동시에 다른 CPU 코어에서 실행

 

스레드 안전성:

  - 코루틴: 항상 메인 스레드에서 실행되므로 쓰레드 안전성 문제가 없음

  - 멀티스레딩: 동시에 여러 쓰레드가 같은 데이터에 접근할 수 있어 race condition 등의 문제가 발생할 수 있음

 

성능:

  - 코루틴: CPU 바운드 작업에는 적합하지 않지만, I/O 바운드 작업이나 시간 지연이 필요한 작업에 효율적

  - 멀티쓰레딩: CPU 바운드 작업에 적합하며, 실제로 병렬 처리가 가능

 

사용 복잡도:

  - 코루틴: 상대적으로 사용하기 쉽고, 유니티의 생명주기와 잘 통합됨

  - 멀티쓰레딩: 더 복잡하며, 동기화, 데드락 방지 등 추가적인 고려사항이 필요

 

 

7. 마무리

마지막 멀티스레딩은 C++ 서버를 공부하던 내용입니다.

유니티에서도 멀티스레드를 사용할 수 있긴 하지만, 기본적으로 단일스레드 동작이라 유니티만 쓰는 사람이면 당장은 크게 신경 안 써도 될듯합니다.

'Unity' 카테고리의 다른 글

Delegate, Action, Event  (1) 2024.11.07
벡터  (0) 2024.11.01
Null 관련 연산자  (3) 2024.10.31
Rigidbody.velocity 천천히 떨어지는 문제  (0) 2024.04.21