Posts C# 비동기 Task를 사용하면서 흔히 발생하는 실수
Post
Cancel

C# 비동기 Task를 사용하면서 흔히 발생하는 실수

Mistake


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static void Main()
{
    Task t = Task.Run(() => TaskBody(3));

    t.Wait(); // TaskBody(3)의 종료를 대기하려고 시도

    Console.WriteLine("End");
}

static async void TaskBody(int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(500);
        Console.WriteLine($"[{i}] Thread : {Thread.CurrentThread.ManagedThreadId}");
    }
}

위의 소스 코드는 얼핏 보면 문제가 없어 보인다.

하지만 실제로 실행하면 t.Wait() 부분에서 원하는 대로 대기가 되지 않는 것을 알 수 있다.


Reason


Task.Run(Action) 메소드는 실행할 메소드 핸들을 받아

스레드풀의 큐에 집어넣고 실행시킨다.

그리고 이 Action의 종료를 대기할 수 있는 Task를 리턴한다.

여기서 이미 힌트가 있다.

Action의 종료를 대기할 수 있다는 것은,

Action의 로직 자체는 동기적으로 수행되어야 한다는 것이다.


그리고 위의 코드에서 이 Action을 살펴보면 다음과 같다.

() => { TaskBody(3); }

실행 블록만 떼어 늘여보면 다음과 같다.

1
2
3
{
    TaskBody(3);
}

TaskBody 메소드는 async void로 비동기 메소드이다.

그러니까 이 메소드 호출 TaskBody(3);은 호출자 스레드 내에서

비동기적으로(Concurrently) 수행된다.


이제 위의 실행 블록을 다시 살펴보면 흐름을 다음과 같이 두 개로 분리할 수 있다.

1
2
3
4
5
6
// [1] 메인 흐름
{
}

// [2] 분기된 비동기 흐름
TaskBody(3);

그러니까 TaskBody(3); 자체를 Action에 꽂아 넣어봐야

Action의 메인 흐름에서 분리되어 비동기적으로 수행된다는 것이다.


다시 Task.Run(() => TaskBody(3))을 살펴보면,

Task.Run(Action)Action의 종료를 대기할 수 있는 핸들을 제공하는데

TaskBody(3)은 여기에서 Action의 메인 흐름과 별개로 떨어져 나가므로

Task t = Task.Run(...)에서 얻어낸 t를 기다려봐야,

시작하자마자 대기가 끝나는 것이다.


Solution 1


1
2
3
4
5
6
7
8
static async Task TaskBody(int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(500);
        Console.WriteLine($"[{i}] Thread : {Thread.CurrentThread.ManagedThreadId}");
    }
}

async void TaskBody(int)에서 voidTask로 바꾼다.

이렇게 되면 TaskBody 메소드 자체가 대기 가능한 Task를 리턴한다.

그리고 Task.Run(Action) 대신 Task.Run(Func<Task>) 메소드로 이를 받아주게 되고,

Task.Run(Func<Task>)메소드는 내부의 Func<Task> 실행을 대기할 수 있는 Task를 리턴하게 된다.

그리고 여전히 TaskBody 메소드는 비동기 메소드이므로,

해당 메소드의 작업을 수행할 스레드가 이미 동기적으로 다른 작업을 하고 있더라도

Concurrent하게 함께 처리할 수 있다.

이런 것까지 고려하고 있었다면 완벽히 원하던 목표인 셈이다.


Solution 2


1
2
3
4
5
6
7
8
static void TaskBody(int count)
{
    for (int i = 0; i < count; i++)
    {
        Thread.Sleep(500);
        Console.WriteLine($"[{i}] Thread : {Thread.CurrentThread.ManagedThreadId}");
    }
}

비동기 키워드를 모두 지워버린다.

그리고 await Task.Delay() 대신 Thread.Sleep()을 사용한다.

이렇게 되면 정석적인 스레드 바디로서 사용되고

Concurrent가 아닌, Parallel한 실행이므로

해당 스레드에서는 동시에 다른 작업을 수행할 수 없지만

Action의 메인 흐름과 함께 동작하므로, 대기할 수 있게 된다.

This post is licensed under CC BY 4.0 by the author.