dev-writeup-2024 / march

개발 1일 1글 스터디
2 stars 0 forks source link

[03-04] Unity Unitask #3

Open Longseabear opened 4 months ago

Longseabear commented 4 months ago

Unitask 정리

Unity unitask는 unity에서 비동기 프로그래밍을 더효율적으로 처리하기 위해 설계된 경량의 비동기/대기 프레임워크 입니다. 기존 C# 'async'와 'await' 패턴을 활용하지만, unity의 메인 스레드와 통합되도록 최적화 되어있는 unity 전용 라이브러리라고 생각하면 됩니다.

Unitask란?

Unitask는 unity용 비동기 프로그래밍 라이브러리로, unity의 'Coroutine'이나 기존의 'async/await'을 대체할 수 있는 효율적인 방법을 제공합니다. 게임개발에서 많은 비동기 작업들이 메인 게임 루프와 밀접하게 연결되어있을 때 유용하게 사용할 수 있습니다.

Unitask는 성능 최적화를 제공하며 메모리 할당을 최소화하여 가비지 컬렉션을 줄입니다.

Unitask의 주요 특징

C# Async/Await

C#에서 async/await 메서드는 "비동기 함수"로 불립니다. 기존에 존재하던 Task의 장점을 살려 개발자가 좀 더 쉽게 비동기 작업을 수행할 수 있도록 만든 것이 async/await입니다.

await 연산자는 피연산자가 나타내는 비동기 작업이 완료될 때 까지 수행을 중지합니다. 그리고 await 연산자를 포함한 메서드에 async를 붙여 컴파일러가 해당 함수가 비동기 함수임을 알 수 있게 해줍니다.

async/await 키워드는 "Task"를 사용하므로 여러 스레드를 활용하는게 특징입니다.

Async/await is a syntactic sugar built on top of Promises to make it easier to work with asynchronous code

중요한 특징은 async/await은 syntax sugar라는 점입니다. 즉, async/await 키워드 없이도 이를 구현할 수 있습니다!

다음 코드가 있다고 가정합니다.

static class DBExecutor
{
    static public async Task<string> ExecuteDB(string spName, long accountId)
    {
        string result;

        Console.WriteLine($"DB 연결 시작");
        await Task.Delay(2000);
        Console.WriteLine($"{spName} 호출 시작.");
        await Task.Delay(2000);
        Console.WriteLine($"{spName} 호출 종료.");

        result = "장형이";

        return result;
    }
}

이를 reverse enginerring을 통해 decompile 시키면,

internal static class DBExecutor
{
    public static Task<string> ExecuteDB(string spName, long accountId)
    {
        ExecuteDBState stateMachine = new ExecuteDBState();
        stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
        stateMachine.spName = spName;
        stateMachine.accountId = accountId;
        stateMachine.state = -1;
        stateMachine.builder.Start(ref stateMachine);
        return stateMachine.builder.Task;
    }

    private sealed class ExecuteDBState : IAsyncStateMachine
    {
        public int state;
        public AsyncTaskMethodBuilder<string> builder;
        public string spName;
        public long accountId;
        private string result;
        private TaskAwaiter awaiter;

        void IAsyncStateMachine.MoveNext()
        {
            int currentState = this.state;
            string result;
            try
            {
                TaskAwaiter awaiter1;
                int nextState;
                TaskAwaiter awaiter2;
                switch (currentState)
                {
                    case 0:
                        awaiter1 = this.awaiter;
                        this.awaiter = new TaskAwaiter();
                        this.state = nextState = -1;
                        break;
                    case 1:
                        awaiter2 = this.awaiter;
                        this.awaiter = new TaskAwaiter();
                        this.state = nextState = -1;
                        goto EndProc;
                    default:
                        Console.WriteLine("DB 연결 시작");
                        awaiter1 = Task.Delay(2000).GetAwaiter();
                        if (!awaiter1.IsCompleted)
                        {
                            this.state = nextState = 0;
                            this.awaiter = awaiter1;
                            ExecuteDBState stateMachine = this;
                            this.builder.AwaitUnsafeOnCompleted(ref awaiter1, ref stateMachine);
                            return;
                        }
                        break;
                }
                awaiter1.GetResult();
                Console.WriteLine(this.spName + " 호출 시작.");
                awaiter2 = Task.Delay(2000).GetAwaiter();
                if (!awaiter2.IsCompleted)
                {
                    this.state = nextState = 1;
                    this.awaiter = awaiter2;
                    ExecuteDBState stateMachine = this;
                    this.builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
                    return;
                }
            EndProc:
                awaiter2.GetResult();
                Console.WriteLine(this.spName + " 호출 종료.");
                this.result = "장형이";
                result = this.result;
            }
            catch (Exception ex)
            {
                this.state = -2;
                this.result = (string)null;
                this.builder.SetException(ex);
                return;
            }
            this.state = -2;
            this.result = (string)null;
            this.builder.SetResult(result);
        }
    }
}

와 같이 변환됩니다. 즉, 컴파일러가 async가 붙은 함수를 class로 구현된 finite state machine으로 변환합니다.

재밌는 건, 구현된 클래스의 메서드를 실행하는 주체가 컴파일러라는 점입니다. 작성한 ExecuteDB async함수의 실행부가 완전히 달라진 것을 확인할 수 있습니다.

Unitask

Task는 C#의 "Awaitable pattern"에 대한 구현체이고, Unitask 또한 C#의 "Awaitable pattern"의 구현체입니다. 따라서, Task와 unitask는 async/await 용법이 같을 뿐 전혀 다르게 동작합니다. 즉, C#에서는 비동기 메커니즘이 어떻게 동작할지에 대한 구현체를 직접 구현할 수 있습니다. async/await은 단순히 async 함수를 await을 분기로하는 state machine으로 변경해줍니다.

다음 시간에는 직접 custom awaitable pattern을 구현하는 LEapsTask를 만들어 더 깊게 이해하는시간을 가져봅니다.

Kjm04175 commented 4 months ago

요즘 개인 프로젝트를 하면서 유니티를 만질 일이 있는데, 연관이 있어 흥미롭게 읽었습니다.

snaag commented 4 months ago

유니태스크라고 읽나요 유니타스크라고 읽나요?

Longseabear commented 3 months ago

유니태스크라고 읽어요~~ 유니태스크 써라 정민아~~